0%

{"created_time":"2021-11-03T06:03:52Z","files":[{"attachment_folder":"","created_time":"2021-11-03T06:03:52Z","id":"349","modified_time":"2021-11-03T06:03:52Z","name":"clion 阅读调试Android native源码.md","signature":"7870016005704","tags":[]},{"attachment_folder":"","created_time":"2021-11-03T06:03:52Z","id":"350","modified_time":"2021-11-03T06:03:52Z","name":"gdb stl支持.md","signature":"89959725934152","tags":[]},{"attachment_folder":"","created_time":"2021-11-03T06:03:52Z","id":"351","modified_time":"2021-11-03T06:03:52Z","name":"github.md","signature":"18525829867080","tags":[]},{"attachment_folder":"","created_time":"2021-11-03T06:03:52Z","id":"352","modified_time":"2021-11-03T06:03:52Z","name":"vscode调试c++.md","signature":"119212748187208","tags":[]},{"attachment_folder":"","created_time":"2021-11-03T06:03:52Z","id":"353","modified_time":"2021-11-03T06:03:52Z","name":"小米入职笔记.md","signature":"121639404709448","tags":[]},{"attachment_folder":"","created_time":"2021-11-03T06:03:52Z","id":"354","modified_time":"2021-11-03T06:03:52Z","name":"工作内容.md","signature":"84002606294600","tags":[]},{"attachment_folder":"","created_time":"2021-11-03T06:03:52Z","id":"355","modified_time":"2021-11-03T06:03:52Z","name":"述职报告.md","signature":"101560432600648","tags":[]}],"folders":[],"id":"348","modified_time":"2021-11-03T06:03:52Z","signature":"46421642454600","version":2}

开发即时通讯软件麦通TM客户端程序
主要负责麦通客户端程序的界面相关部分, 主要工作有客户端界面常用组件模块的子类化, 聊天编辑内容解析.交互动画设计,qss皮肤设计.UI自动布局,模型层数据展示,常用组件扩展自定义委托等.

项目经验
2015年9月-2017年1月
负责展讯平台ota升级开发维护工作
负责recovery子系统/ota脚本制作/编译部分的开发和维护工作.
开发物理分区自适应调整, 一篇专利<<一种新的OTA升级物理分区自适应处理方案>>
华为出差支持emui sd卡升级方案.
recovery中不需要挂载system启动adb服务.
android L/M/N 升级适配工作.

2018年1月-至今
负责展讯平台USB-framework部分开发维护工作
device端管理/音频框架(包括midi服务)/host管理/usb数据传输/typec端口管理/(相关cts案例, 通过fw-usb框架实现上层直接读取otg存储)
/配件模式开发/网络tethering相关linux网络服务/pc-share功能.
usb网络逆绑定feature(手机使用pc的网络).

2017年1月-至今
负责展讯平台存储framework的开发维护工作, 涉及到部分和存储交互的pkms的工作.
涉及到存储视图, fuse 用户态文件系统, 文件加密fde/fbe工作, SAF框架.
ums功能开发优化.
Android升级后提供应用开发指导.

1. push代码

push相关的命令
git push

1
2
3
4
5
git remote -v 查看project名称
git log --decorate 查看推送分支
eg:
git push ssh://zhangliguang@gerrit.pt.miui.com:29418/miui/bootable/recovery
miui/v10-p-secureboot-dev

2. 手机版本信息

E7-红米5Plus 是 vince v9-o-sakura-dev.xml
D2-小米手机5X 是 tiffany v9-o-sakura-dev.xml
A4-小米note2 否 scorpio bsp-scorpio-o.xml ?
C3B-红米手机5A 是 riva v10-o-riva-dev.xml?

F10-DAVINCI(davinci) http://corgi.pt.miui.com/open-api/build/946878/out/davinci_images_1.1.1_20190515.0000.00_9.0_cn_bd142a7436.tgz

F7B-Redmi Note 7 Pro(violet) http://corgi.pt.miui.com/open-api/build/946880/out/violet_images_1.1.1_20190515.0000.00_9.0_cn_08ef449855.tgz

E2-Mi 8 SE(sirius) http://corgi.pt.miui.com/open-api/build/946884/out/sirius_images_1.1.1_20190515.0000.00_9.0_cn_ad77fd2275.tgz

E5G-MI MIX3 5G(andromeda) http://corgi.pt.miui.com/open-api/build/946889/out/andromeda_images_1.1.1_20190515.0000.00_9.0_cn_fdbe783bf9.tgz

F7A-Redmi Note 7(lavender) http://corgi.pt.miui.com/open-api/build/946891/out/lavender_images_1.1.1_20190515.0000.00_9.0_cn_49bb4e955f.tgz

E1-Mi 8(dipper) http://corgi.pt.miui.com/open-api/build/946892/out/dipper_images_1.1.1_20190515.0000.00_9.0_cn_4827ff7e87.tgz
D1S-Redmi 6 Pro(sakura) http://corgi.pt.miui.com/open-api/build/946893/out/sakura_images_1.1.1_20190515.0000.00_9.0_cn_16d788434d.tgz

F6-Redmi Y3(onc) http://corgi.pt.miui.com/open-api/build/947025/out/onc_images_1.1.1_20190515.0000.00_9.0_cn_cd1bd6bd8e.tgz

E4-MI MAX 3(nitrogen) http://corgi.pt.miui.com/open-api/build/947169/out/nitrogen_images_1.1.1_20190515.0000.00_8.1_cn_28f1c8cbb1.tgz

D2-Mi 5X(tiffany) http://corgi.pt.miui.com/open-api/build/948226/out/tiffany_images_1.1.1_20190516.0000.00_8.1_cn_126c6c0956.tgz

D9/D9P-MIPAD 4(clover) http://corgi.pt.miui.com/open-api/build/948230/out/clover_images_1.1.1_20190516.0000.00_8.1_cn_dba47da306.tgz

Redmi 5A(riva) http://corgi.pt.miui.com/open-api/build/948249/out/riva_images_1.1.1_20190516.0000.00_8.1_cn_4605ee89ed.tgz

2.1. 适配androidq recovery

2.1.1. 遗留问题

  1. 无法进入recovery, 不显示黑屏或ui界面 –需要在init中添加log调试,需要check 新版本上怎样进入recovery的。
  2. kernel command line 好像不对, 需要看下为什么没传入进去
  3. 查找漏掉的提交
  4. 下载v10代码
预处理 操作步骤 预期结果 实际结果
在主系统下 adb reboot 正常进入miuirecovery,界面正常显示,资源dpi/语言都正常 同预期
在miui recovery下 选择重启手机 可以正常重启到主系统 同预期
在miui recovery下 选择清除数据->清空所有数据->确认 显示清除数据的进度条,完成后显示清除成功 同预期
在miui recovery下 选择连接小米助手 显示连接界面,连接adb后,pc端打开小米助手,显示连接的设备,点击清除数据,手机端可以清除数据;点击重启设备,可以重启手机 同预期

2.2. recovery q ota适配

2.2.1. uncrypt selinux问题

06-11 14:08:27.656 15035 15035 I /system/bin/uncrypt: update package is “/data/media/0/miui_CEPHEUS_9.6.11_965ed9e148_10.0.zip”
06-11 14:08:27.657 15035 15035 I /system/bin/uncrypt: encryptable: yes
06-11 14:08:27.657 15035 15035 I /system/bin/uncrypt: encrypted: yes
06-11 14:08:27.657 15035 15035 I /system/bin/uncrypt: writing block map /cache/recovery/block.map
06-11 14:08:27.657 15035 15035 I /system/bin/uncrypt: block size: 4096 bytes
06-11 14:08:27.657 15035 15035 I /system/bin/uncrypt: file size: 2531725768 bytes, 618098 blocks
06-11 14:08:27.657 15035 15035 E /system/bin/uncrypt: failed to open /data/media/0/miui_CEPHEUS_9.6.11_965ed9e148_10.0.zip for reading: Permission denied
06-11 14:08:27.657 1586 4812 I RecoverySystemService: uncrypt read status: 0
06-11 14:08:27.645 15035 15035 I auditd : type=1400 audit(0.0:890): avc: denied { write } for comm=”uncrypt” name=”miui_CEPHEUS_9.6.11_965ed9e148_10.0.zip” dev=”sda32” ino=147560 scontext=u:r:uncrypt:s0 tcontext=u:object_r:media_rw_data_file:s0 tclass=file permissive=0
06-11 14:08:27.645 15035 15035 W uncrypt : type=1400 audit(0.0:890): avc: denied { write } for name=”miui_CEPHEUS_9.6.11_965ed9e148_10.0.zip” dev=”sda32” ino=147560 scontext=u:r:uncrypt:s0 tcontext=u:object_r:media_rw_data_file:s0 tclass=file permissive=0
06-11 14:08:27.658 1586 4812 E RecoverySystemService: uncrypt failed with status: -1
06-11 14:08:27.659 4716 15033 W System.err: java.io.IOException: process package failed
06-11 14:08:27.659 4716 15033 W System.err: at android.os.RecoverySystem.processPackage(RecoverySystem.java:477)
06-11 14:08:27.659 4716 15033 W System.err: at android.os.RecoverySystem.processPackage(RecoverySystem.java:500)
06-11 14:08:27.659 4716 15033 W System.err: at com.android.updater.q.u(SourceFile:1)
06-11 14:08:27.659 4716 15033 W System.err: at com.android.updater.Updater2.a(SourceFile:327)
06-11 14:08:27.660 4716 15033 W System.err: at com.android.updater.Updater2.a(SourceFile:4)
06-11 14:08:27.660 4716 15033 W System.err: at com.android.updater.Updater2$a.doInBackground(SourceFile:7)
06-11 14:08:27.660 4716 15033 W System.err: at com.android.updater.Updater2$a.doInBackground(SourceFile:1)
06-11 14:08:27.660 4716 15033 W System.err: at android.os.AsyncTask$3.call(AsyncTask.java:378)
06-11 14:08:27.660 4716 15033 W System.err: at java.util.concurrent.FutureTask.run(FutureTask.java:266)
06-11 14:08:27.660 15035 15035 I /system/bin/uncrypt: received 0, exiting now
06-11 14:08:27.660 4716 15033 W System.err: at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:289)
06-11 14:08:27.661 4716 15033 W System.err: at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)

2.2.2. 升级不解析问题

update_package不解析

[ 0.411717] Command: “/system/bin/recovery” “--update_package=@/cache/recovery/block.map” “–locale=zh_CN” “–export_validate=MqadHEZDflODfQymzyqxoin5ObXM+ieSH6RqmjgUQMu6i3bF4NoxS/0GukMykWgH57fLV4zuAgqehWoijO9k7Tl06zZvhvYl+OkHeeilLg7Vlc+gOeP8fzaRXNCj9us30lOrZkjXvWmC9HJjc+lbU2IKO4t31VHGdH+FzPik5TdkexJ47iW9+G4oUo5cmuKU/d7QwzOkwwz4SQAjtCn1hBkdzKD6lYFJSEDt/zM9iMcMUkogokIkslIybVWeqYW3R9POcXqZnidWG11lyp+3EP5iNEN+aIANRvaVXGEUeChkEIr/bGdfNBKbGI/VfQj3aah5psJTSBmAJgz6J8ZZAQ==”

/system/bin/recovery --update_package=@/cache/recovery/block.map –locale=zh_CN –export_validate=MqadHEZDflODfQymzyqxoin5ObXM+ieSH6RqmjgUQMu6i3bF4NoxS/0GukMykWgH57fLV4zuAgqehWoijO9k7Tl06zZvhvYl+OkHeeilLg7Vlc+gOeP8fzaRXNCj9us30lOrZkjXvWmC9HJjc+lbU2IKO4t31VHGdH+FzPik5TdkexJ47iW9+G4oUo5cmuKU/d7QwzOkwwz4SQAjtCn1hBkdzKD6lYFJSEDt/zM9iMcMUkogokIkslIybVWeqYW3R9POcXqZnidWG11lyp+3EP5iNEN+aIANRvaVXGEUeChkEIr/bGdfNBKbGI/VfQj3aah5psJTSBmAJgz6J8ZZAQ==

is_battery_ok check不过, get_health_service有问题

[ 169.924703] W:Waited for hwservicemanager.ready for a second, waiting another…

#签名校验不过
[ 1.363049] Decrypt identity start MqadHEZDflODfQymzyqxoin5ObXM+ieSH6RqmjgUQMu6i3bF4NoxS/0GukMykWgH57fLV4zuAgqehWoijO9k7Tl06zZvhvYl+OkHeeilLg7Vlc+gOeP8fzaRXNCj9us30lOrZkjXvWmC9HJjc+lbU2IKO4t31VHGdH+FzPik5TdkexJ47iW9+G4oUo5cmuKU/d7QwzOkwwz4SQAjtCn1hBkdzKD6lYFJSEDt/zM9iMcMUkogokIkslIybVWeqYW3R9POcXqZnidWG11lyp+3EP5iNEN+aIANRvaVXGEUeChkEIr/bGdfNBKbGI/VfQj3aah5psJTSBmAJgz6J8ZZAQ==
[ 1.363304] Decrypt identity successed.
[ 1.363370] check_identity start
[ 1.363436] Get device rom zone.
[ 1.363513] Get serial number from miphone.
[ 1.363594] Open /proc/serial_num failed by No such file or directory ,retry from prop.
[ 1.363662] Get package’s version.
[ 1.363728] Get package’s sha bay MemMapping.
[ 6.974783] msg:dev
[ 6.974800] version=>1.1.1:eng.mi.20190604.213145
[ 6.974803] SN=>0x6f371ea2:0x6f371ea2
[ 6.974805] SHA=>9ce8528f8d037b32904975dcb6b918adfe1bb7ba:9ce8528f8d037b32904975dcb6b918adfe1bb7ba
[ 6.974868] Failed to verify message.
[ 6.974912] E:secureboot check_identity failed.

不能使用自己编的recovery img升级, 会比对recovery的版本, 可以先将version去掉

2.2.3. 关掉 rescue party

adb shell setprop persist.sys.disable_rescue true

2.2.4. 抓取offline log

offlinelog

1
2
3
setprop persist.sys.offlinelog.kernel true
setprop persist.sys.offlinelog.logcat true
/data/local/log下

p 开发版

1
*#*#6335463#*#*   log存放在/data/log下

2.3. ota升级包本地测试

repo sync platform/build;
将imgdiff拷贝到/build/tools/releasetools文件夹下
build/tools/releasetools

提交代码:
repo sync platform/tools/repohooks

下载releasetools的目录
qcom的在device下
还有的在芯片目录下
device/xiaomi/sdm710_common/

安装bsdiff
制作差分包的命令:

1
./ota_from_target_files  --no_signing   -s /home/mi/work_space/v10-catus-dev/vendor/mediatek/proprietary/scripts/releasetools/releasetools.py  -i ~/下载/signed_cactus-target_files-9.6.9-9.0.zip         ~/下载/signed_cactus-target_files-9.6.10.root-9.0.zip   ~/下载/ota_test.zip

releasetools.py使用的目标版本的target-files中的,所以如果需要替换,需要重新更改
可以指定target-files解压目录

1
./build/tools/releasetools/ota_from_target_files --block --no_signing -s ~/work_space/q9/device/xiaomi/sm8250_common/releasetools.py    --extracted_input_target_files  ~/Downloads/signed_umi-target_files-9.8.28-10.0    ~/Downloads/signed_umi-target_files-9.8.28-10.0.zip   ota_test.zip

androidq的是 –extracted_input_target_files

制作差分包时需要指定source 的解压目录和 target的解压目录
制作包时比较快(需要合入change 617757)

1
./ota_from_target_files --block --no_signing -s ~/work_space/v10-grus-p-dev/device/xiaomi/sdm710_common/releasetools.py  --extracted_source_target_files ~/Downloads/signed_grus-target_files-9.6.13.root-9.0   -i  ~/Downloads/signed_grus-target_files-9.6.13.root-9.0.zip  --extracted_input_target_files ~/Downloads/signed_grus-target_files-9.6.14.root-9.0  ~/Downloads/signed_grus-target_files-9.6.14.root-9.0.zip     ota_test2_inc.zip
1
./ota_from_target_files --block --no_signing -s ~/work_space/miui-r-umi-dev/bootable/recovery/etc/qcom/releasetools.py  --extracted_source_target_files ~/Downloads/signed_umi-target_files-20.6.18-11.0   -i  ~/Downloads/signed_umi-target_files-20.6.18-11.0.zip  --extracted_input_target_files ~/Downloads/signed_umi-target_files-20.6.28-11.0  ~/Downloads/signed_umi-target_files-20.6.28-11.0.zip    ota_test_inc.zip

2.4. 1217功能适配

将vold hwservicemanager servicemanager拷贝到recovery中.
先看下缺少的动态库有哪些.

  1. vold

android.hardware.keymaster@3.0.so
android.hardware.keymaster@4.0.so
libbinder.so

2.5. F2问题追踪

/cache调试

1
dd if=/data/recovery.img of=/dev/block/bootdevice/by-name/recovery
1
2
3
130|grus:/vendor/etc # logcat | grep avc | grep install_recovery                                            01-01 01:07:34.499  5766  5766 I applypatch: type=1400 audit(0.0:1739): avc: denied { read } for name="sda27" dev="tmpfs" ino=5478 scontext=u:r:install_recovery:s0 tcontext=u:object_r:cache_block_device:s0 tclass=blk_file permissive=1
01-01 01:07:34.499 5766 5766 I applypatch: type=1400 audit(0.0:1740): avc: denied { open } for path="/dev/block/sda27" dev="tmpfs" ino=5478 scontext=u:r:install_recovery:s0 tcontext=u:object_r:cache_block_device:s0 tclass=blk_file permissive=1
install_recovery cache_block_device blk_file r_file_perms

2.6. cache问题追踪

cache_size 268435456 阈值0.8,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ ll miui-blockota-lavender-9.6.14.root-9.6.17.root-8165a765ef-9.0.zip      
-rw-rw-r-- 1 mi mi 316244556 7月 31 17:43 miui-blockota-lavender-9.6.14.root-9.6.17.root-8165a765ef-9.0.zip
$ ll
总用量 300864
drwxrwxr-x 5 mi mi 4096 7月 31 20:07 .
drwxr-xr-x 156 mi mi 53248 7月 31 20:07 ..
-rw-rw-r-- 1 mi mi 8331 1月 1 2009 compatibility.zip
drwxrwxr-x 2 mi mi 4096 7月 31 20:07 firmware-update
drwxrwxr-x 3 mi mi 4096 7月 31 20:07 META-INF
drwxrwxr-x 2 mi mi 4096 7月 31 20:07 patch
-rw-rw-r-- 1 mi mi 189640704 1月 1 2009 system.new.dat
-rw-rw-r-- 1 mi mi 95192104 1月 1 2009 system.patch.dat
-rw-rw-r-- 1 mi mi 567815 1月 1 2009 system.transfer.list
-rw-rw-r-- 1 mi mi 790528 1月 1 2009 vendor.new.dat
-rw-rw-r-- 1 mi mi 21281022 1月 1 2009 vendor.patch.dat
-rw-rw-r-- 1 mi mi 517922 1月 1 2009 vendor.transfer.list
$ grep stash system.transfer.list| wc -l
1057
$ grep new system.transfer.list| wc -l
137
$ grep move system.transfer.list| wc -l
3835

cache_size 237536000 阈值0.8,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
mi@ubuntu:~/work_space/v10-p-lavender-dev/build/tools/releasetools$ ll  ota_test.zip 
-rw-rw-r-- 1 mi mi 424622171 7月 31 20:00 ota_test.zip
-rw-r--r-- 1 mi mi 8331 1月 1 2009 compatibility.zip
drwxrwxr-x 2 mi mi 4096 7月 31 20:03 firmware-update/
drwxrwxr-x 3 mi mi 4096 7月 31 20:03 META-INF/
drwxrwxr-x 2 mi mi 4096 7月 31 20:03 patch/
-rw-r--r-- 1 mi mi 368357376 1月 1 2009 system.new.dat
-rw-r--r-- 1 mi mi 94693506 1月 1 2009 system.patch.dat
-rw-r--r-- 1 mi mi 535087 1月 1 2009 system.transfer.list
-rw-r--r-- 1 mi mi 790528 1月 1 2009 vendor.new.dat
-rw-r--r-- 1 mi mi 21289216 1月 1 2009 vendor.patch.dat
-rw-r--r-- 1 mi mi 514813 1月 1 2009 vendor.transfer.list
mi@ubuntu:~/work_space/v10-p-lavender-dev/build/tools/releasetools/ota_test$ grep stash system.transfer.list | wc -l
955
mi@ubuntu:~/work_space/v10-p-lavender-dev/build/tools/releasetools/ota_test$ grep new system.transfer.list | wc -l
397
mi@ubuntu:~/work_space/v10-p-lavender-dev/build/tools/releasetools/ota_test$ grep move system.transfer.list | wc -l
3627

对system观察直观上看就是数据量new dat变大了, stash变少了, new变多了

2.7. 编译相关

zsh shell下编译使用source build/envsetup.sh, 后面include的脚本会使用zsh进行解析. 有些命令zsh和bash间并不是兼容的, 所以最正确的做法还是使用bash build/envsetup.sh, 强制使用bash进行脚本解析. 这样可以规避掉不兼容的问题

1
bash build/envsetup.sh
1
./build/tools/releasetools/ota_from_target_files --block --no_signing -s ~/work_space/q9/device/xiaomi/sm8250_common/releasetools.py  --extracted_src_target_files ~/Downloads/umi-target_files-1.1.7-10.0_1931592bfa   -i ~/Downloads/umi-target_files-1.1.7-10.0_1931592bfa.zip --extracted_input_target_files  ~/Downloads/signed_umi-target_files-9.9.3.root-10.0 ~/Downloads/signed_umi-target_files-9.9.3.root-10.0.zip   ota_test_inc.zip

2.8. 配置串口log

1
CONFIG_SERIAL_MSM_GENI_CONSOLE=y

  1. Android 平台项目USB/STORAGE/PKMS维护工作

1.平台内部bug快速响应, 及时解决(主要涉及AndroidO AndroidP及小内存4.4项目)
2.积累梳理易错问题点,收敛bug 增强方案健壮性。
3.关联模块做好沟通宣导工作

1 重点掌握usb模块的自研feature. 梳理bug, 整理易错点,积极同上下游模块(mtp/driver/adb)做好沟通协调。对不合理的地方进行调整. 如usb的typec功能依赖方面进行调整, 对usb sim卡激活功能进行重新适配. 通过与上下层(audio和usb driver)协调, 适配midi服务框架等.

2 storage模块的bug相对减少, 碰到的问题大多是卸载关机时长性能,格式化问题/迁移数据/格式化内部存储, 多是底层的变化或使用存储不规范引起. 同时会有一些因android升级引起的问题, 如一些cts问题, 有些为android原生的问题, 如存储uuid接口规范的变化, 提供了规避方案.

3 处理pkms模块bug, 提供给上层的pkms接口使用指导, 原生接口使用存在问题等, 给应用同事提供使用建议. 对统计应用信息用户信息的行为进行了深入调查并输出文档.及时同下游模块做好沟通协调.

4 对一些疑问比较多的点,做了归纳整理, 如android storage访问权限的变更, 和文件系统层在低存储时对进程的限制策略, sdcardfs层的工作原理进行了调研并输出文档.

  1. Android9 平台项目USB模块 porting工作
  • FW-USB框架系统调研(音频框架(包括midi服务)\host管理\usb数据传输\typec端口管理(相关cts案例, 通过fw-usb框架实现上层直接读取otg存储)\配件模式开发\网络tethering相关linux网络服务)
  • 将平台的新feature适配Android9.0 上。对新的Android版本上的限制, 做好转化适配.
  • 不同的芯片系列的不同方案做好适配工作.
  • 根据对新版本的解读, 协助上下游模块做好适配工作.
  • 及时发现google原生问题, 提交google issue及解决方案
  • 做到porting结束后, 测试提出的bug因本模块导致的问题少, 极少出现严重问题, 模块稳定工作.
  1. 梳理usb模块的差异对比, 对fw usb整体上涉及的各个知识点进行了系统性调查和深入. 如usb模块涉及到的alsa音频框架服务,编写demo掌握midi端口使用规范,midi信息传输, 产生midi音频的apk行为等进行研究. typec端口上下层服务hidl服务. 新增的Usb Gadget HAL源码解读及行为预研工作. 新增的安全锁屏的行为及应用场景等. 对之前涉及较少的usb配件模式开发编写demo, 熟悉其控制方式等. 对usb网络相关的两个重要feature tethering/pc-share功能进行了深入, 输出文档, 同时深入android网络相关的服务, 对涉及到的linux网络运维方面的知识(dns,子网,路由,nat转发等)进行了研究并输出相关文档. 对usb存储设备, 调研非节点挂载方案(不经过vold服务), 直接通过usb的相关接口进行访问的apk的行为及代码解读.

  2. 对usb device端的变化及新增特性拆解, 适应新版本变化.

  3. 对device功能依赖typec的行为及切换主控受控端等方面, 提交了google issue, 推动 google解决处理, 协调上游模块适配typec功能开发.

  4. Android9 平台项目STORAGE模块 porting工作

  • 对存储机制变化较大的部分进行系统解读.
    应用模块存储机制变化, 主要涉及SAF框架(client-DocumentUI-provider协同工作解读), 中间层文件系统由fuse转换为sdcardfs, 对该文件系统相关源码深化学习.
    应用磁盘配额管理, 统计应用使用信息的变化等
  • 上下游模块协同适配
  • 对google原生设计中存在不合理的地方, 提交google issue及修改方案
  • 根据对新版本的整体解读, 将新feature按要求合入.
  • 做到porting结束后, 测试提出的bug因本模块导致的问题少, 模块稳定工作.
  1. 对quota文件系统及sdcardfs文件系统的研究, 对其相关行为进行调研. 协助文件系统相关同事进行适配.

  2. 因androidp上对存储权限收紧, apk访问需要通过SAF框架访问非apk所属文件. 对SAF框架从上到下整体方案进行了系统性调研, 涉及到DocumentUi, DocumentProvider, StorageManagerService vold sdcardfs等相关知识.

  3. 输出SAF新框架与quota加入后的data空间管理机制方面的文档.

  4. 低存储fw保护机制调研开发

  • 开发前期参考对比机表现,制定开发计划。
  • 开发过程中及时发现新问题,合入并验收通过。
  • 发布测试文档/研发使用指导文档。

1 重点参考华为小存储手机上做的优化,根据其表现结合展讯手机的特性制定了开发方案.
涉及到几个点:
a. 存储容量下限多个阈值的机制, 通知上层应用根据阈值采取不同的限制策略;
b. 不同项目的阈值线不同;c. 不同阈值情况下, 检测频率不同, 通知用户的频率也不同,且通知频率与检测频率不同.在不给用户造成烦扰的情况下合理利用手机资源;在最低阈值下, 用户必须清理空间, 如不清理,限制用户使用手机(如home键屏蔽\back键只能返回到存储管理界面等),以避免手机存储不足导致功能异常;d.如多次检测时处于同一阈值区间, 只发送一次提醒.
2 开发过程中发现了多个问题,及时调整;如activity重复开启问题, service与activity通知频率不一致问题,关闭提醒后,提醒的频率变动等.方案做的更细致,逻辑更加清晰,提升了用户据体验.
3 合入版本前,进行了充分测试,并提供了测试用例\测试指导文档\研发使用指导文档\客户指导文档等.

  1. 客户厂商product分区fingerprint方案调研适配
  • 理清现有fingerprint的编译过程, fingerprint的作用机制
  • 找出可行的修改方案, 并对不同修改方案造成的影响面进行评估, 讨论商定最终方案
  • 对修改方案进行整体测试验收

从编译系统上找出影响fingerprint变化的机制, 对系统中存在的四个fingerprint的作用机制进行调研.make命令触发fingerprint更新.
对fingerprint从系统功能上/ota升级层面/google cts/gts/vts 多个方面进行系统调研, 找出最合理的product fingerprint适配方案
通过多个维度, 对多对一的product分区适配系统版本的方案的多种特殊情况进行拆解, 找出必须的适配路径, 给相关同事提供参考建议.

{"created_time":"2021-11-03T06:03:52Z","files":[{"attachment_folder":"","created_time":"2021-11-03T06:03:52Z","id":"336","modified_time":"2021-11-03T06:03:52Z","name":"Android fuse 文件系统介绍.md","signature":"110528324314696","tags":[]},{"attachment_folder":"","created_time":"2021-11-03T06:03:52Z","id":"337","modified_time":"2021-11-03T06:03:52Z","name":"Android Storage 存储架构分析1.md","signature":"77800673519176","tags":[]},{"attachment_folder":"","created_time":"2021-11-03T06:03:52Z","id":"338","modified_time":"2021-11-03T06:03:52Z","name":"AndroidQ 存储sandbox.md","signature":"50257048249928","tags":[]},{"attachment_folder":"","created_time":"2021-11-03T06:03:52Z","id":"339","modified_time":"2021-11-03T06:03:52Z","name":"new_note.md","signature":"134502831760968","tags":[]},{"attachment_folder":"","created_time":"2021-11-03T06:03:52Z","id":"340","modified_time":"2021-11-03T06:03:52Z","name":"new_note_001.md","signature":"88782904895048","tags":[]},{"attachment_folder":"","created_time":"2021-11-03T06:03:52Z","id":"341","modified_time":"2021-11-03T06:03:52Z","name":"SAF框架.md","signature":"24852316694088","tags":[]},{"attachment_folder":"","created_time":"2021-11-03T06:03:52Z","id":"342","modified_time":"2021-11-03T06:03:52Z","name":"test.md","signature":"52670819870280","tags":[]},{"attachment_folder":"","created_time":"2021-11-03T06:03:52Z","id":"343","modified_time":"2021-11-03T06:03:52Z","name":"低存储机制说明.md","signature":"18525829867080","tags":[]},{"attachment_folder":"","created_time":"2021-11-03T06:03:52Z","id":"344","modified_time":"2021-11-03T06:03:52Z","name":"开机log相关.md","signature":"104605564413512","tags":[]},{"attachment_folder":"","created_time":"2021-11-03T06:03:52Z","id":"345","modified_time":"2021-11-03T06:03:52Z","name":"文件系统初探.md","signature":"133940191045192","tags":[]},{"attachment_folder":"","created_time":"2021-11-03T06:03:52Z","id":"346","modified_time":"2021-11-03T06:03:52Z","name":"磁盘配额调研报告.md","signature":"86128615106120","tags":[]},{"attachment_folder":"","created_time":"2021-11-03T06:03:52Z","id":"347","modified_time":"2021-11-03T06:03:52Z","name":"统计app总使用空间.md","signature":"3918646093384","tags":[]}],"folders":[],"id":"335","modified_time":"2021-11-03T06:03:52Z","signature":"53495453591112","version":2}

sv39

satp: 0x8000000000087fff
va: 0x80000fac

求 pa

sv39
38…30 9bit
29…21 9bit
20…12 9bit
11…0 12bit

64bit 中一个页表项占 64bit,一个 double word

  • satp.PPN 给出了一级页表的基址, VA[38:30]给出了一级页号, 因此处理器会读取位于地址(satp.PPN × 4096 + VA[38:30] × 8)的页目录项 (此处的 8 为 8字节,即一个页表项占 64bit)
  • 该 PTE 包含二级页表的基址, VA[29:21]给出了二级页号, 因此处理器读取位于地址(PTE.PPN × 4096 + VA[29:21] × 8)的页目录项
  • 该 PTE 包含了三级页表的基址, VA[20:12] 给出了三级页号, 处理器读取位于地址(PTE.PPN × 4096 + VA[20:12] × 8)的页目录项
  • 该页表项的 PTE. PPN 就是物理地址对应的 PPN * 4096 + offset 得到物理地址

va: 0x80000fac

1
2
3
4
5
bit:   1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 
index: 31| 30| 29| 28| 27| 26| 25| 24| 23| 22| 21| 20| 19| 18| 17| 16|
----------------------------------------------------------------------
bit: 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 0 | 1 | 0 | 1 | 1 | 0 | 0 |
index: 15| 14| 13| 12| 11| 10| 09| 08| 07| 06| 05| 04| 03| 02| 01| 00|

satp.ppn = 0x87fff << 12 = 0x87fff000

  • satp.PPN × 4096 + VA[38:30] × 8 = 0x87fff000+0b10*8 = 0x87fff010 @地址存放的值为 (0x21ffe801)
  • PTE.PPN × 4096 + VA[29:21] × 8 = 0x21ffe801>>10<<12 + 0*8 = 0x87ffa000 @地址存放的值为 (0x21ffe401)
  • PTE.PPN × 4096 + VA[20:12] × 8 = 0x21ffe401>>10<<12 + 0*8 = 0x87ff9000 @地址存放的值为 (0x2000004b)
  • PPN x 4096 + offset = 0x2000004b>>10<<12 + 0xfac = 0x80000fac

最终取得 pa 为 0x80000fac

KVM

KVM(Kernel-based Virtual Machine,基于内核的虚拟机)是一种用于 Linux 内核中的虚拟化基础设施。本质是一个嵌入到 Linux 内核中的虚拟化功能模块 kvm.ko,该模块在利用 Linux 内核所提供的部分操作系统能力(e.g. 任务调度、内存管理、硬件设备交互)的基础上,再加入了处理器和内存虚拟化的能力,使得 Linux 内核具备了成为 VMM 的条件。

KVM 内核模块本身只能提供 CPU 和内存的虚拟化
KVM 需要硬件虚拟化技术支持(Intel VT-x, AMD svm, ARM hypervisor, RISCV H-extension),所以 KVM 也被称之为硬件辅助的虚拟化实现。

严格来说 kvm 属于硬件辅助的全虚拟化 + type2 类虚拟化技术.

KVM 的功能清单:

  • 支持 CPU 和 Memory 超分(Overcommit)
  • 支持半虚拟化 I/O(virtio)
  • 支持热插拔 (CPU、块设备、网络设备等)
  • 支持 SMP(Symmetric Multi-Processing,对称多处理)处理器架构
  • 支持 NUMA (Non-Uniform Memory Access,非一致存储访问)处理器架构
  • 支持实时迁移(Live Migration)
  • 支持 PCI 设备直接分配(Pass-through)和单根 I/O 虚拟化 (SR-IOV)
  • 支持合并相同内存页 (KSM )

KVM 内核模块加载流程

当启动 Linux 操作系统并加载 KVM 内核模块时:

  • 初始化 KVM 模块内部的数据结构
  • KVM 模块检测当前的 CPU 体系结构, 打开虚拟化模式开关
  • KVM 模块创建特殊的接口设备文件 /dev/kvm 并等待来自用户空间(QEMU)的指令。

KVM 是运行在内核态的且本身不能进行任何io设备的模拟。所以,KVM 还必须借助于一个运行在用户态的应用程序来模拟出虚拟机所需要的虚拟设备(e.g. 网卡、显卡、存储控制器和硬盘)同时为用户提供操作入口。目前这个应用程序的最佳选择就是 QEMU。

QEMU

纯软QEMU 本身作为一套完整的 VMM 实现,包括了处理器虚拟化内存虚拟化,以及模拟各类虚拟设备的功能。QEMU 4.0.0 版本甚至几乎可以模拟任何硬件设备,但由于这些模拟都是纯软件实现的,所以其性能低下

在 KVM 开发者在对 QEMU 进行稍加改造后,QEMU 可以通过 KVM 对外暴露的 /dev/kvm 接口来对其进行调用。从 QEMU 角度来看,也可以说是 QEMU 使用了 KVM 的处理器和内存虚拟化功能,为自己的虚拟机提供了硬件辅助虚拟化加速。

image-20240416112227420

虚拟机的配置和创建、虚拟机运行所依赖的虚拟设备、虚拟机运行时的用户环境和用户交互,以及一些虚拟机的特定技术,比如:动态迁移,都是交由 QEMU 来实现的。

QEMU 的使用方式

  1. 纯软件(二进制翻译)实现的全虚拟化虚拟机
  2. 基于硬件辅助虚拟化(KVM)的全虚拟化虚拟机
  3. 基于硬件辅助虚拟化(KVM)的半虚拟化虚拟机 (借助软件virtio 实现io 模拟)
  4. 仿真器:为用户空间的进程提供 CPU 仿真(指令翻译),让在不同处理器结构体系上编译的程序得以跨平台运行。例如:让 RISCV 架构上编译的程序在 x86 架构上运行(借由 VMM 的形式) 。

QEMU-KVM

KVM 官方提供的软件包下载包含了 KVM 内核模块、QEMU、qemu-kvm 以及 virtio, qemu-kvm 本质是专门针对 KVM 的 QEMU 分支代码包

QEMU-KVM 相比原生 QEMU 的改动:

  • 原生的 QEMU 通过指令翻译实现 CPU 的完全虚拟化,但是修改后的 QEMU-KVM 会调用 ICOTL 命令来调用 KVM 模块。
  • 原生的 QEMU 是单线程实现,QEMU-KVM 是多线程实现。

在 QEMU-KVM 中,KVM 运行在内核空间,提供 CPU 和内存的虚级化,以及 Guest OS 的 I/O 拦截。QEMU 运行在用户空间,提供硬件 I/O 虚拟化,并通过 ioctl 调用 /dev/kvm 接口将 KVM 模块相关的 CPU 指令传递到内核中执行。当 Guest OS 的 I/O 被 KVM 拦截后,就会将 I/O 请求交由 QEMU 处理

KVM 提供的ioctl 命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
open("/dev/kvm", O_RDWR|O_LARGEFILE)    = 3
ioctl(3, KVM_GET_API_VERSION, 0) = 12
ioctl(3, KVM_CHECK_EXTENSION, 0x19) = 0
ioctl(3, KVM_CREATE_VM, 0) = 4
ioctl(3, KVM_CHECK_EXTENSION, 0x4) = 1
ioctl(3, KVM_CHECK_EXTENSION, 0x4) = 1
ioctl(4, KVM_SET_TSS_ADDR, 0xfffbd000) = 0
ioctl(3, KVM_CHECK_EXTENSION, 0x25) = 0
ioctl(3, KVM_CHECK_EXTENSION, 0xb) = 1
ioctl(4, KVM_CREATE_PIT, 0xb) = 0
ioctl(3, KVM_CHECK_EXTENSION, 0xf) = 2
ioctl(3, KVM_CHECK_EXTENSION, 0x3) = 1
ioctl(3, KVM_CHECK_EXTENSION, 0) = 1
ioctl(4, KVM_CREATE_IRQCHIP, 0) = 0
ioctl(3, KVM_CHECK_EXTENSION, 0x1a) = 0

QEMU-KVM 调用 KVM 内核模块启动虚拟机的流程概要

  • 获取 /dev/kvm fd(文件描述符)

  • 创建虚拟机,获取虚拟机的句柄

    KVM_CREATE_VM 可以理解成 KVM 为虚拟机创建了对应的数据结构,然后,KVM 会返回一个文件句柄来代表该虚拟机。针对这个句柄执行 ioctl 调用即可完成对虚拟机执行相应的管理,比如:

    • 创建用户空间虚拟地址(Virtual Address)
    • 客户机物理地址GPA(Guest Physical Address)以及主机物理地址HPA(Host Physical Address)之间的映射关系
  • 为虚拟机映射内存和其他的 PCI 设备,以及信号处理的初始化。

    1
    ioctl(kvmfd, KVM_SET_USER_MEMORY_REGION, &mem);
  • 将虚拟机镜像数据映射到内存,相当于物理机的 boot 过程,把操作系统内核映射到内存。

  • 创建 vCPU,并为 vCPU 分配内存空间。KVM_CREATE_VCPU 时,KVM 为每一个 vCPU 生成对应的文件句柄,对其执行相应的 ioctl 调用,就可以对 vCPU 进行管理

    1
    2
    ioctl(kvmfd, KVM_CREATE_VCPU, vcpuid);
    vcpu->kvm_run_mmap_size = ioctl(kvm->dev_fd, KVM_GET_VCPU_MMAP_SIZE, 0);
  • 创建 vCPU 个数的线程并运行虚拟机。

    1
    ioctl(kvm->vcpus->vcpu_fd, KVM_RUN, 0);
  • 线程进入循环,监听并捕获虚拟机退出原因,做相应的处理。这里的退出并不一定指的是虚拟机关机,虚拟机如果遇到 I/O 操作,访问硬件设备,缺页中断等都会执行退出。执行退出可以理解为将 CPU 执行上下文返回到 QEMU。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    open("/dev/kvm")
    ioctl(KVM_CREATE_VM)
    ioctl(KVM_CREATE_VCPU)
    for (;;) {
    ioctl(KVM_RUN)
    switch (exit_reason) { /* 分析退出原因,并执行相应操作 */
    case KVM_EXIT_IO: /* ... */
    case KVM_EXIT_HLT: /* ... */
    }
    }

虚拟化 kvm-qemu io模拟框架

image-20240416112232080

virtio

传统的设备模拟中,虚拟机内部设备驱动完全不知道自己处在虚拟化环境中。对于网络和存储等,I/O操作会完整地走完虚拟机内核栈->QEMU->宿主机内核栈,产生很多的VM Exit和VM Entry,所以性能很差。virtio方案则是旨在提高性能的一种优化方案,在该方案中,虚拟机能够感知到自己处于虚拟化环境,并且会加载相应的virtio总线驱动和virtio设备驱动。

半虚拟化包含两个部分:

  • VMM 创建出模拟的设备 (后端)
  • guest os 安装好该模拟设备的驱动 (前端)

传统的模拟手段, 如模拟网卡收发包过程中, 会配置众多的寄存器和io端口, 每一次都需要陷入陷出VMM, 使得网卡性能较差. 而半虚拟化通过虚拟的设备封装设备请求, 大幅减少陷入陷出的次数.

virtio是一种前后端架构,包括前端驱动(Front-End Driver)和后端设备(Back-End Device)以及自身定义的传输协议。通过传输协议,virtio不仅可以用于QEMU/KVM方案,也可以用于其他的虚拟化方案

前端驱动为虚拟机内部的virtio模拟设备对应的驱动,每一种前端设备都需要有对应的驱动才能正常运行。前端驱动的主要作用是接收用户态的请求,然后按照传输协议将这些请求进行封装,再写I/O端口,发送一个通知到QEMU的后端设备。

后端设备则是在QEMU中,用来接收前端驱动发过来的I/O请求,然后从接收的数据中按照传输协议的格式进行解析,对于网卡等需要实际物理设备交互的请求,后端驱动会对物理设备进行操作,从而完成请求,并且会通过中断机制通知前端驱动

virtio能够支持各种不同的设备,如基于virtio实现的网络架构通常被称为virtio-net

后端设备位于用户态QEMU进程,VCPU 需要暂停执行

image-20240416112239957

virtio设备的初始化

virtio设备首先需要创建一个PCI设备,叫作virtio PCI代理设备,这个代理设备挂到PCI总线上,接着virtio代理设备再创建一条virtio总线,这样virtio设备就可以挂到这条总线上了。

virtio PCI代理的父设备是一个PCI设备,类型为VirtioPCIClass,实例为VirtIOPCIProxy,注意这是一个抽象设备,所以并不能创建其实例,只能由其子类去创建。QEMU中定义了所有virtio设备的PCI代理设备,如virtio balloon PCI设备、virtio scsi PCI设备、virito crypto PCI设备

image-20240416112244530

所有的virtio设备都有一个共同的父类TYPE_VIRTIO_DEVICE

virtio ballon 设备示例

这里以virtio balloon设备为例分析virtio设备的初始化过程。创建virtio balloon时只需要创建其PCI代理设备(即TYPE_VIRTIO_BALLOON_PCI)即可,在命令行指定-device virtio-balloon-pci

通常来说,要改变客户机占用的宿主机内存,是要先关闭客户机,修改启动时的内存配置,然后重启客户机才能实现。而内存的ballooning(气球)技术可以在客户机运行时动态地调整它所占用的宿主机内存资源,而不需要关闭客户机。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
static void virtio_balloon_pci_register(void)
{
virtio_pci_types_register(&virtio_balloon_pci_info);
}
static const VirtioPCIDeviceTypeInfo virtio_balloon_pci_info = {
.base_name = TYPE_VIRTIO_BALLOON_PCI,
.generic_name = "virtio-balloon-pci",
.transitional_name = "virtio-balloon-pci-transitional",
.non_transitional_name = "virtio-balloon-pci-non-transitional",
.instance_size = sizeof(VirtIOBalloonPCI),
.instance_init = virtio_balloon_pci_instance_init,
.class_init = virtio_balloon_pci_class_init,
};
static void virtio_balloon_pci_class_init(ObjectClass *klass, void *data)
{
DeviceClass *dc = DEVICE_CLASS(klass);
VirtioPCIClass *k = VIRTIO_PCI_CLASS(klass);
PCIDeviceClass *pcidev_k = PCI_DEVICE_CLASS(klass);
k->realize = virtio_balloon_pci_realize; // 具现
set_bit(DEVICE_CATEGORY_MISC, dc->categories);
device_class_set_props(dc, virtio_balloon_pci_properties);
pcidev_k->vendor_id = PCI_VENDOR_ID_REDHAT_QUMRANET;
pcidev_k->device_id = PCI_DEVICE_ID_VIRTIO_BALLOON;
pcidev_k->revision = VIRTIO_PCI_ABI_VERSION;
pcidev_k->class_id = PCI_CLASS_OTHERS;
}

QEMU在main函数中会对所有-device的参数进行具现化,设备的具现化函数都会调用device_set_realized函数,在该函数中会调用设备类的realize函数

virtio设备类的继承链关系为DeviceClass->PCIDeviceClass->VirtioPCIClass

image-20240416112248800

image-20240416112252260

virtio驱动的加载

由于virtioPCI代理设备的存在,PCI进行扫描的时候会扫描到这个设备,并且会调用相应驱动的probe函数,virtio_pci_driver及其probe回调函数定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
static struct pci_driver virtio_pci_driver = {
.name = "virtio-pci",
.id_table = virtio_pci_id_table,
.probe = virtio_pci_probe, // probe 实现
.remove = virtio_pci_remove,
.sriov_configure = virtio_pci_sriov_configure,
};
static int virtio_pci_probe(struct pci_dev *pci_dev,
const struct pci_device_id *id)
{
struct virtio_pci_device *vp_dev, *reg_dev = NULL;
int rc;
vp_dev = kzalloc(sizeof(struct virtio_pci_device), GFP_KERNEL);
pci_set_drvdata(pci_dev, vp_dev);
vp_dev->vdev.dev.parent = &pci_dev->dev;
vp_dev->vdev.dev.release = virtio_pci_release_dev;
vp_dev->pci_dev = pci_dev;
INIT_LIST_HEAD(&vp_dev->virtqueues);
spin_lock_init(&vp_dev->lock);

/* enable the device */
rc = pci_enable_device(pci_dev); // pci_enable_device使能该PCI设备
if (rc)
goto err_enable_device;
...
rc = virtio_pci_modern_probe(vp_dev); // 初始化该PCI设备对应的virtio设备
if (rc == -ENODEV)
rc = virtio_pci_legacy_probe(vp_dev); // 初始化该PCI设备对应的virtio设备
if (rc)
goto err_probe;
...
pci_set_master(pci_dev);

vp_dev->is_legacy = vp_dev->ldev.ioaddr ? true : false;

rc = register_virtio_device(&vp_dev->vdev);
reg_dev = vp_dev;
if (rc)
goto err_register;

return 0;
err_register:
... // err
return rc;
}

调用pci_enable_device使能该PCI设备,接下来调用virtio_pci_legacy_probe或者virtio_pci_modern_probe来初始化该PCI设备对应的virtio设备,只考虑modern设备,virtio_pci_modern_probe代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
int virtio_pci_modern_probe(struct virtio_pci_device *vp_dev)
{
struct virtio_pci_modern_device *mdev = &vp_dev->mdev;
struct pci_dev *pci_dev = vp_dev->pci_dev;
int err;

mdev->pci_dev = pci_dev;

err = vp_modern_probe(mdev); // 进入 vp_modern_probe
if (err)
return err;

if (mdev->device)
vp_dev->vdev.config = &virtio_pci_config_ops;
else
vp_dev->vdev.config = &virtio_pci_config_nodev_ops;

vp_dev->config_vector = vp_config_vector;
vp_dev->setup_vq = setup_vq;
vp_dev->del_vq = del_vq;
vp_dev->isr = mdev->isr;
vp_dev->vdev.id = mdev->id;

return 0;
}
int vp_modern_probe(struct virtio_pci_modern_device *mdev)
{
struct pci_dev *pci_dev = mdev->pci_dev;
int err, common, isr, notify, device;
u32 notify_length;
u32 notify_offset;

check_offsets();
// 设置device id
if (pci_dev->device < 0x1040) {
mdev->id.device = pci_dev->subsystem_device;
} else {
mdev->id.device = pci_dev->device - 0x1040;
}
// 设置vendor id
mdev->id.vendor = pci_dev->subsystem_vendor;

// 接下来调用多次virtio_pci_find_capability来发现virtio PCI代理设备的pci capability,这也是在virtio_pci_device_plugged写入到virtio PCI代理设备的配置空间中的

/* check for a common config: if not, use legacy mode (bar 0). */
common = virtio_pci_find_capability(pci_dev, VIRTIO_PCI_CAP_COMMON_CFG,
IORESOURCE_IO | IORESOURCE_MEM,
&mdev->modern_bars);
...

/* If common is there, these should be too... */
isr = virtio_pci_find_capability(pci_dev, VIRTIO_PCI_CAP_ISR_CFG,
IORESOURCE_IO | IORESOURCE_MEM,
&mdev->modern_bars);
notify = virtio_pci_find_capability(pci_dev, VIRTIO_PCI_CAP_NOTIFY_CFG,
IORESOURCE_IO | IORESOURCE_MEM,
&mdev->modern_bars);
...
err = dma_set_mask_and_coherent(&pci_dev->dev, DMA_BIT_MASK(64));
device = virtio_pci_find_capability(pci_dev, VIRTIO_PCI_CAP_DEVICE_CFG,
IORESOURCE_IO | IORESOURCE_MEM,
&mdev->modern_bars);

err = pci_request_selected_regions(pci_dev, mdev->modern_bars,
"virtio-pci-modern");
...
err = -EINVAL;
// 调用map_capability将对应的capability在PCI代理设备中的BAR空间映射到内核地址空间,如virtio_pci_device的common成员就映射了virtio_pci_common_cfg的数据到内核中,这样,后续就可以直接通过这个内存地址空间来访问common这个capability了,其他的capability类似。这样实际上就将virtio PCI代理设备的BAR映射到虚拟机内核地址空间了,后续直接访问这些地址即可实现对virtio PCI代理设备的配置和控制
mdev->common = vp_modern_map_capability(mdev, common,
sizeof(struct virtio_pci_common_cfg), 4,
0, sizeof(struct virtio_pci_common_cfg),
NULL, NULL);
...
mdev->isr = vp_modern_map_capability(mdev, isr, sizeof(u8), 1,
0, 1,
NULL, NULL);
...

/* Read notify_off_multiplier from config space. */
pci_read_config_dword(pci_dev,
notify + offsetof(struct virtio_pci_notify_cap,
notify_off_multiplier),
&mdev->notify_offset_multiplier);
/* Read notify length and offset from config space. */
pci_read_config_dword(pci_dev,
notify + offsetof(struct virtio_pci_notify_cap,
cap.length),
&notify_length);

pci_read_config_dword(pci_dev,
notify + offsetof(struct virtio_pci_notify_cap,
cap.offset),
&notify_offset);


if ((u64)notify_length + (notify_offset % PAGE_SIZE) <= PAGE_SIZE) {
mdev->notify_base = vp_modern_map_capability(mdev, notify,
2, 2,
0, notify_length,
&mdev->notify_len,
&mdev->notify_pa);
...
} else {
mdev->notify_map_cap = notify;
}

// virtio_pci_modern_probe函数接着设置virtio_pci_device中virtio_device的成员vdev的config成员。如果有device这个capability,则设置为virtio_pci_config_ops,设置virtio_pci_device的几个回调函数
if (device) {
mdev->device = vp_modern_map_capability(mdev, device, 0, 4,
0, PAGE_SIZE,
&mdev->device_len,
NULL);
...
}

return 0;

err_map_device:
// ... err
}
EXPORT_SYMBOL_GPL(vp_modern_probe);

首先设置了virtio设备的vendor ID和device ID,值得注意的是,virtio PCI代理设备的device ID就是上一节中在virtio_pci_device_plugged函数中设置的0x1040+5,所以这里virtio设备的device ID为5。virtio_pci_modern_probe函数接下来调用多次virtio_pci_find_capability来发现virtio PCI代理设备的pci capability,这也是在virtio_pci_device_plugged写入到virtio PCI代理设备的配置空间中的,virtio_pci_find_capability找到所属的PCI BAR,写入到virtio_pci_device的modern_bars成员中,从QEMU的virtio_pci_realize函数中可以知道这个modern_bars是1<<4。接着pci_request_selected_regions就将virtio PCI代理设备的BAR地址空间保留起来了

virtio_pci_modern_probe函数调用map_capability将对应的capability在PCI代理设备中的BAR空间映射到内核地址空间,如virtio_pci_device的common成员就映射了virtio_pci_common_cfg的数据到内核中,这样,后续就可以直接通过这个内存地址空间来访问common这个capability了,其他的capability类似。这样实际上就将virtio PCI代理设备的BAR映射到虚拟机内核地址空间了,后续直接访问这些地址即可实现对virtio PCI代理设备的配置和控制。virtio_pci_modern_probe函数接着设置virtio_pci_device中virtio_device的成员vdev的config成员。如果有device这个capability,则设置为virtio_pci_config_ops,设置virtio_pci_device的几个回调函数,config_vector与MSI中断有关,setup_vq用来配置virtio设备virt queue,del_vq用来删除virt queue

执行完 virtio_pci_modern_probe 后, 注册的函数指针:

image-20240416112259350

virtio_pci_modern_probe返回之后会调用register_virtio_device,这个函数将一个virtio device注册到系统中

register_virtio_device函数设置virtio设备的Bus为virtio_bus,virtio_bus在系统初始化的时候会注册到系统中。设置virtio设备的名字为类似virtio0virtio1的字符串,然后调用dev->config->reset回调函数重置设备,最后调用device_register将设备注册到到系统中。device_register函数跟设备驱动相关比较大,这里简单介绍一下其作用。该函数会调用device_add将设备加到系统中,并且会发送一个uevent消息到用户空间,这个uevent消息中包含了virtio设备的vendor iddevice id,udev接收到这个消息之后会加载virtio设备的对应驱动(动态驱动的情况下, 如果是打包到kernel的驱动, 这一步则不需要, 直接跳过即可)。device_add会调用bus_probe_device,最终调用到Bus的probe函数和设备的probe函数,也就是virtio_dev_probe和virtballoon_probe函数

一般来讲,virtio驱动初始化一个设备的过程如下:

1)重置设备,这是在上述register_virtio_device函数中通过dev->config->reset调用完成的。

2)设置ACKNOWLEDGE状态位,表示virtio驱动已经知道了该设备,这同样是在register_virtio_device函数中由add_status(dev,VIRTIO_CONFIG_S_ACKNOWLEDGE语句完成的。

3)设置DRIVER状态位,表示virtio驱动知道怎么样驱动该设备,这是在virtio总线的probe函数virtio_dev_probe中通过add_status(dev,VIRTIO_CONFIG_S_DRIVER)完成的。

4)读取virtio设备的feature位,求出驱动设置的feature,将两者计算子集,然后向设备写入这个子集特性,这是在virtio_dev_probe函数中完成的,计算driver_features和device_features,然后调用virtio_finalize_features

5)设置FEATURES_OK特性位,这之后virtio驱动就不会再接收新的特性了,这一步是在函数virtio_finalize_features中通过调用add_status(dev,VIRTIO_CONFIG_S_FEATURES_OK)完成的

6)重新读取设备的feature位,确保设置了FEATURES_OK,否则设备不支持virtio驱动设置的一些状态,表示设备不可用,这同样是在virtio_finalize_features函数中完成的。

7)执行设备相关的初始化操作,包括发现设备的virtqueue、读写virtio设备的配置空间等,这是在virtio_dev_probe函数中通过调用驱动的probe函数完成的,即drv->probe(dev)。

8)设置DRIVER_OK状态位,这通常是在具体设备驱动的probe函数中通过调用virtio_device_ready完成的,对于virtio balloon来说是virtballoon_probe,如果设备驱动没有设置DRIVER_OK位,则会由总线的probe函数virtio_dev_probe来设置

virtio 设备驱动加载过程分析

  1. 启动虚拟机

    image-20240416112303889

  2. 在虚拟机中使用auditd对驱动访问进行监控

    image-20240416112307138

  3. 在虚拟机中调用udevadm monitor对uevent事件进行监控

    image-20240416112309964

  4. 添加virtio-rng-pci设备

    image-20240416112313355

udev可以看到有设备添加的消息

image-20240416112316272

再从audit的日志看,udev确实加载了virtio-rng.ko驱动

image-20240416112319969

使用lspci-v可以看到,所有virtio设备的驱动均为virtio_pci,并没有virtio-rng、virtio-net等驱动

image-20240416112324004

因为virtio设备是由一个PCI的控制器添加的,其本质是一个virtio设备,会挂到virtio总线上,所以PCI总线上只会显示其驱动为virtio-pci

virtio 驱动初始化

先说一下驱动加载过程中的virtio_pci_config_ops, 被赋值给了 virtio_device 结构体的config 成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static const struct virtio_config_ops virtio_pci_config_ops = {
.get = vp_get,
.set = vp_set,
.generation = vp_generation,
.get_status = vp_get_status,
.set_status = vp_set_status,
.reset = vp_reset,
.find_vqs = vp_modern_find_vqs,
.del_vqs = vp_del_vqs,
.synchronize_cbs = vp_synchronize_vectors,
.get_features = vp_get_features,
.finalize_features = vp_finalize_features,
.bus_name = vp_bus_name,
.set_vq_affinity = vp_set_vq_affinity,
.get_vq_affinity = vp_get_vq_affinity,
.get_shm_region = vp_get_shm_region,
.disable_vq_and_reset = vp_modern_disable_vq_and_reset,
.enable_vq_after_reset = vp_modern_enable_vq_after_reset,
};

virtio_pci_config_ops结构体中的成员函数通常是代理virtioPCI代理设备的I/O操作,包括读写virtio PCI代理设备的PIO和MMIO,如get_status和set_status成员对应的vp_get_status和vp_set_status函数

1
2
3
4
+ vp_set_status(vdev, status)
\ -+ vp_modern_set_status(&vp_dev->mdev, status);
\ - *cfg = mdev->common
| - vp_iowrite8(status, &cfg->device_status);

vp_dev->mdev->common对应的是virtio PCI代理设备第四个BAR表示的地址中的一段空间,其指向的数据表示如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct virtio_pci_common_cfg {
/* About the whole device. */
__le32 device_feature_select; /* read-write */
__le32 device_feature; /* read-only */
__le32 guest_feature_select; /* read-write */
__le32 guest_feature; /* read-write */
__le16 msix_config; /* read-write */
__le16 num_queues; /* read-only */
__u8 device_status; /* read-write */
__u8 config_generation; /* read-only */

/* About a specific virtqueue. */
__le16 queue_select; /* read-write */
__le16 queue_size; /* read-write, power of 2. */
__le16 queue_msix_vector; /* read-write */
__le16 queue_enable; /* read-write */
__le16 queue_notify_off; /* read-only */
__le32 queue_desc_lo; /* read-write */
__le32 queue_desc_hi; /* read-write */
__le32 queue_avail_lo; /* read-write */
__le32 queue_avail_hi; /* read-write */
__le32 queue_used_lo; /* read-write */
__le32 queue_used_hi; /* read-write */
};

上面代码中的每一个成员都表示一个virtio PCI代理设备modern MMIO地址空间中对应的值,读写这些成员都会陷入到QEMU中, 比如设置或者获取设备状态的device_status成员, 该MemoryRegion对应的回调操作结构是common_ops.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
--> hw/virtio/virtio-pci.c
static void virtio_pci_modern_regions_init(VirtIOPCIProxy *proxy)
{
static const MemoryRegionOps common_ops = {
.read = virtio_pci_common_read,
.write = virtio_pci_common_write,
.impl = {
.min_access_size = 1,
.max_access_size = 4,
},
.endianness = DEVICE_LITTLE_ENDIAN,
};
...
}

virtio_pci_config_ops的各个函数封装了这些I/O操作, virtio 设备可以通过这个结构体中的各个回调函数来驱动设备.

virtio balloon 设备驱动初始化示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
+ virtballoon_probe(virtio_device *vdev)
\ - virtio_balloon *vb = kzalloc(sizeof(*vb), GFP_KERNEL)
| - INIT_WORK(&vb->update_balloon_stats_work, update_balloon_stats_func);
| - balloon_devinfo_init(&vb->vb_dev_info);
| -+ init_vqs(vb);
\ - callbacks[VIRTIO_BALLOON_VQ_INFLATE] = balloon_ack;
| - callbacks[VIRTIO_BALLOON_VQ_DEFLATE] = balloon_ack;
| -+ virtio_find_vqs(vb->vdev, VIRTIO_BALLOON_VQ_MAX, vqs, callbacks, names, NULL)
\ -+ vdev->config->find_vqs(vdev, nvqs, vqs, callbacks, names, NULL, desc)
\ -+ vp_modern_find_vqs(vdev, nvqs, vqs, callbacks, names, NULL, desc)
\ -+ vp_find_vqs(vdev, nvqs, vqs, callbacks, names, ctx, desc)
\ -| vp_find_vqs_msix(vdev, nvqs, vqs, callbacks, names, true, ctx, desc);
| -| vp_find_vqs_msix(vdev, nvqs, vqs, callbacks, names, false, ctx, desc);
| -+ vp_find_vqs_intx(vdev, nvqs, vqs, callbacks, names, ctx)
\ - vp_dev->vqs = kcalloc(nvqs, sizeof(*vp_dev->vqs), GFP_KERNEL)
| - request_irq(vp_dev->pci_dev->irq, vp_interrupt, IRQF_SHARED,
dev_name(&vdev->dev), vp_dev) "申请中断"
| -+ qs[i] = vp_setup_vq(vdev, queue_idx++, callbacks[i], names[i],
ctx ? ctx[i] : false, VIRTIO_MSI_NO_VECTOR);
\ -+ vq = vp_dev->setup_vq(vp_dev, info, index, callback, name, ctx,
msix_vec);
\ - vp_modern_get_num_queues(mdev)
| - num = vp_modern_get_queue_size(mdev, index)
| - !num | vp_modern_get_queue_enable(mdev, index)
| - vq = vring_create_virtqueue(index, num,
SMP_CACHE_BYTES, &vp_dev->vdev, true, true, ctx,
vp_notify, callback, name);
| - vp_modern_set_queue_enable(&vp_dev->mdev, [for vq in vqs]->index, true);
| - vb->inflate_vq = vqs[VIRTIO_BALLOON_VQ_INFLATE];
| - vb->deflate_vq = vqs[VIRTIO_BALLOON_VQ_DEFLATE];
| - virtio_device_ready(vdev); "设置driver ok 状态位"

vp_find_vqs函数本质上只调用了一个函数vp_try_to_find_vqs,但是3次调用的参数不同。3次调用的区别主要是virtio设备使用中断的方式,vp_try_to_find_vqs函数的最后两个参数一个是是否使用MSIx的中断方式,另一个是如果使用MSIx中断方式,最后一种是否是每个virtqueue一个vector。virtio设备是否使用MSIx,是由QEMU中virtio PCI代理设备结构VirtIOPCIProxy中的nvectors决定的,而这个值是作为属性添加的. 如virtio PCI代理设备的属性virtio_crypto_pci_properties定义中有DEFINE_PROP_UINT32("vectors",VirtIOPCIProxy,nvectors,2),这句代码表示virtio crypto有两个MSIx的vector。virtio pci balloon设备没有定义这个属性,所以还是使用传统的INTx中断方式,也就是所有的中断都使用一个中断

vp_request_intx申请了一个中断资源,中断处理函数为vp_interrupt, 中断申请之后会对每一个virtqueue调用vp_setup_vq来初始化virtqueue, 这个回调函数同样是在virtio_pci_modern_probe中设置的,为setup_vq

首先得到virtio_pci_device的common成员,这是virtio PCI代理设备中用来配置的一段MMIO,直接读写这些地址会导致陷入到QEMU中的virtio_pci_common_read/write函数

结合setup_vq的代码,可以总结初始化一个virtqueue的步骤:

  1. 如果判断需要初始化的virtqueue的索引大于读取出来的队列,那就返回错误,对应到QEMU中,通过判断VirtQueue中vring成员num(也就是virtqueue)大小不为零来判断队列个数
  2. 读取队列大小,队列大小不能为0并且该队列不处于enable状态
  3. 读取queue_notify_off寄存器的值,这个值表示virtio驱动在通知virtio设备后端时应该写的地址在notify_base中的偏移,QEMU只是简单以队列的索引返回,所以进行通知时,只需要队列索引号*notify_offset_multiplier即可。
  4. 调用alloc_virtqueue_pages分配virtqueue的页面,本质上就是分配vring的descriptor table、available ring和used ring 3个部分,这个3个部分是在连续的物理地址空间中,“info->queue”保存了分配空间的虚拟地址。
  5. 调用vring_new_virtqueue创建一个vring_virtqueue结构,参数中的vp_notify表示virtio驱动用来通知virtio设备的函数,callback表示virtio设备使用了descriptor table之后virtio驱动会调用的函数。vring_virtqueue的第一个成员是virtqueue结构,vring_virtqueue包含了所有virtqueue的信息,vring_new_virtqueue即是用来分配vring_virtqueue的,值得注意的是还多分配了num个void*指针,这是用来在调用使用通知时传递的所谓token。vring_new_virtqueue中还会调用vring_init,这个函数初始化vring,设置vring中队列大小(vring->num)、descriptor table(vring->desc)、avail ring(vring->avail)和used ring(vring->used)的地址。值得注意的是,vring_new_virtqueue会把每个vring_desc的next成员设置为下一个vring_desc的索引
  6. 激活队列,这个步骤会把队列大小,队列的descriptor table、avail ring和used ring的物理地址写入到相应的寄存器中。
  7. 设置virtqueue的priv成员为notify地址。对于virtio balloon来说,notify_offset_multiplier为4个字节, 当virtio驱动调用vp_notify通知virtio设备时,会直接写vq->priv地址, QEMU这边只需要将地址除以notify_offset_multiplier即可找到对应的队列

virtio_balloon设备的init_vqs函数调用之后,相关的数据结构

image-20240416112329901

virtqueue机制

  • 处理批量的异步I/O请求
  • 减少上下文切换次数
  • 基于共享内存机制

Virtqueue机制具体实现—vring

  • 描述符表:保存一系列描述符,每一个描述符都被用来描述一块客户机内的内存区域
  • avail ring:保存后端设备可以使用的描述符
  • used ring:后端驱动已经处理过并且尚未反馈给前端驱动的描述符

image-20240416112333572

virtqueue 机制虽然一定程度上改善了性能, 但是由于io 数据发送接收过程仍然涉及到多次上下文切换, qemu 用户态的virtio的后端需要集中处理来自guest的io 请求, 最后再从host 用户态发给内核态处理这些io请求.

例如 guest 发包给外部网络,首先,guest 需要切换到 host kernel,然后 host kernel 会切换到 qemu 用户态来处理 guest 的请求, Hypervisor 通过系统调用将数据包发送到外部网络后,会切换回 host kernel , 最后再切换回 guest。这样漫长的路径无疑会带来性能上的损失。vhost 正是在这样的背景下提出的一种改善方案

image-20240416112336510

vhost

vhost 是位于 host kernel 的一个模块,用于和 guest 直接通信,数据交换直接在 guest 和 host kernel 之间通过 virtqueue 来进行,qemu 不参与通信,但也没有完全退出舞台,它还要负责一些控制层面的事情,比如和 KVM 之间的控制指令的下发等。

以vhost-net 内核模块举例, 它以一个独立的模块完成 guest 和 host kernel 的数据交换过程。

初始化过程:

  • 通过ioctl初始化vhost-net

image-20240416112340081

guest 通知 host kernel 中vhost-net 模块的事件要借助 host kernel的 kvm.ko 模块来完成

vhost-net 初始化期间,会启动一个工作线程 work 来监听 eventfd,一旦 guest 对vhost-net 发出event,kvm.ko 触发 ioeventfd 通知到 vhost-netvhost-net 通过 virtqueue 的 avail ring 获取数据,并设置 used ring。同样,从 vhost 工作线程向 guest 通信时,也采用同样的机制,只不过这种情况发的是一个回调的 call envent,kvm.ko 触发 irqfd 通知 guest。

可以看到这种架构下, 从guest 中虚拟io设备发送数据到 host的 真实io 设备

  • 通信过程不需要进入到host的用户态处理
  • 异步处理, 多核cpu上不需要终止vcpu的执行, 避免了上下文开销
  • 不影响前端驱动的设计, guest os 不需要额外的修改

Vhost-user架构

Vhost从host 内核迁移到host用户态,一般集成在DPDK等用户态驱动中

采用 UNIX 域套接字来建立QEMU进程与vhost-user之间的联系,进而初始化vhost-user

事件通知机制与Vhost-net相同

数据在用户态传递

image-20240416112343837

VFIO

设备直通就是将物理设备直接挂到虚拟机,虚拟机通过直接与设备交互来获得较好的性能。传统的透传设备到QEMU/KVM虚拟机的方法为PCI passthrough,这种老的设备直通方式需要KVM完成大量的工作,如与IOMMU交互、注册中断处理函数等。显然这种方法会让KVM过多地与设备打交道,扮演一个设备驱动的角色,这种方案不够通用灵活,所以后来有了VFIO(Virtual Function I/O)

VFIO是一个用户态驱动框架,它利用硬件层面的I/O虚拟化技术,如Intel的VT-d和AMD的AMD-Vi,将设备直通给虚拟机。传统上,设备驱动与设备进行交互需要访问设备的很多资源,如PCI设备的配置空间BAR地址空间设备中断等,所有这些资源都是在内核态进行分配和访问的。虚拟化环境下,把设备直通给虚拟机之后,QEMU需要接管所有虚拟机对设备资源的访问。

VFIO的基本思想包括两个部分

  • 将物理设备的各种资源分解,并将获取这些资源的接口向上导出到用户空间
    • QEMU等应用层软件可以利用这些接口获取硬件的所有资源,包括设备的配置空间、BAR空间和中断
  • 聚合,也就是将从硬件设备得到的各种资源聚合起来,对虚拟化展示一个完整的设备接口,这种聚合是在用户空间完成
    • 它从硬件设备分解各种资源之后,会重新聚合成一个虚拟机设备挂到虚拟机上,QEMU还会调用KVM的接口将这些资源与虚拟机联系起来,使得虚拟机内部完全对VFIO的存在无感知,虚拟机内部的操作系统能够透明地与直通设备进行交互,也能够正常处理直通设备的中断请求

image-20240416112347325

  • VFIO Interface作为接口层,用来向应用层导出一系列接口,QEMU等用户程序可以通过相应的ioctl对VFIO进行交互
  • iommu driver是物理硬件IOMMU的驱动实现,如Intel和AMD的IOMMU
  • pci_bus driver是物理PCI设备的驱动程序
  • vfio_iommu是对底层iommu driver的封装,用来向上提供IOMMU的功能,如DMA Remapping以及Interrupt Remapping
  • vfio_pci是对设备驱动的封装,用来向用户进程提供访问设备驱动的功能,如配置空间和模拟BAR

VFIO的重要功能之一是对各个设备进行分区,但是即使有IOMMU的存在,想要以单个设备作为隔离粒度有时也做不到。所以,VFIO设备直通中有3个重要的概念,即containergroupdevice,其关系如图所示

image-20240416112350504

group是IOMMU能够进行DMA隔离的最小单元,一个group内可能只有一个device,也可能有多个device,这取决于物理平台上硬件的IOMMU拓扑结构。设备直通的时候一个group里面的设备必须都直通给一个虚拟机。不能让一个group里的多个device分别从属于2个不同的VM,也不允许部分device在宿主机上而另一部分被分配到虚拟机里,因为这样一个虚拟机中的device可以利用DMA攻击获取另外一个虚拟机里的数据,无法做到物理上的DMA隔离。

device指的是要操作的硬件设备,不过这里的“设备”需要从IOMMU拓扑的角度去理解。如果该设备是一个硬件拓扑上独立的设备,那么它自己就构成一个IOMMU group。如果这里是一个multi-function设备,那么它和其他的function一起组成一个IOMMU group,因为多个function设备在物理硬件上是互联的,它们可以互相访问数据,所以必须放到一个group里隔离起来

container是由多个group组成的,虽然group是VFIO的最小隔离单元,但是有的时候并不是最好的分割粒度。如多个group可能会共享一组页表,通过将多个group组成一个container可以提高系统的性能,也能够方便用户。一般来讲,每个进程/虚拟机可以作为一个container。

image-20240416112353598

VFIO使用方法

  1. 假设需要直通的设备如下所示

    image-20240416112358776

  2. 找到这个设备的VFIO group,这是由内核生成的

    image-20240416112402860

  3. 查看group里面的设备,这个group只有一个设备

    image-20240416112405858

  4. 将设备与驱动程序解绑

    image-20240416112409452

  5. 找到设备的生产商&设备ID

    image-20240416112412904

  6. 将设备绑定到vfio-pci驱动,这会导致一个新的设备节点“/dev/vfio/15”被创建,这个节点表示直通设备所属的group文件,用户态程序可以通过该节点操作直通设备的group

    image-20240416112417061

  7. 修改这个设备节点的属性

    image-20240416112420366

  8. 设置能够锁定的内存为虚拟机内存+一些IO空间

    image-20240416112423822

  9. 向QEMU传递相关参数

    image-20240416112427019

KVM上层管理工具

一个成熟的虚拟化解决方案离不开良好的管理和运维工具,部署、运维、管理的复杂度与灵活性是企业实施虚拟化时重点考虑的问题。KVM目前已经有从libvirt API、virsh命令行工具到OpenStack云管理平台等一整套管理工具,尽管与老牌虚拟化巨头VMware提供的商业化虚拟化管理工具相比在功能和易用性上有所差距,但KVM这一整套管理工具都是API化的、开源的,在使用的灵活性以及对其做二次开发的定制化方面仍有一定优势。

libvirt

libvirt是使用最广泛的对KVM虚拟化进行管理的工具和应用程序接口,已经是事实上的虚拟化接口标准,作为通用的虚拟化API,libvirt不但能管理KVM,还能管理VMware、Hyper-V、Xen、VirtualBox等其他虚拟化方案。

virsh

virsh是一个常用的管理KVM虚拟化的命令行工具,对于系统管理员在单个宿主机上进行运维操作,virsh命令行可能是最佳选择。virsh是用C语言编写的一个使用libvirt API的虚拟化管理工具,其源代码也是在libvirt这个开源项目中的。

virt-manager

virt-manager是专门针对虚拟机的图形化管理软件,底层与虚拟化交互的部分仍然是调用libvirt API来操作的。virt-manager除了提供虚拟机生命周期(包括:创建、启动、停止、打快照、动态迁移等)管理的基本功能,还提供性能和资源使用率的监控,同时内置了VNC和SPICE客户端,方便图形化连接到虚拟客户机中。virt-manager在RHEL、CentOS、Fedora等操作系统上是非常流行的虚拟化管理软件,在管理的机器数量规模较小时,virt-manager是很好的选择。因其图形化操作的易用性,成为新手入门学习虚拟化操作的首选管理软件。

OpenStack

OpenStack是一个开源的基础架构即服务(IaaS)云计算管理平台,可用于构建共有云和私有云服务的基础设施。OpenStack是目前业界使用最广泛的功能最强大的云管理平台,它不仅提供了管理虚拟机的丰富功能,还有非常多其他重要管理功能,如:对象存储、块存储、网络、镜像、身份验证、编排服务、控制面板等。OpenStack仍然使用libvirt API来完成对底层虚拟化的管理。

问题背景

在使用最新版本 qemu (8.0) 搭配 linux kernel (5.19.16) 模拟出的 riscv 主机上, 使用 riscv 版本的 qemu 跑 kvm-mode 的 guest kernel 无法执行.
现象为没有 host kernel 的 log, 没有 guest kernel 的 log 输出.

问题初步分析

在 kvm_riscv_vcpu_enter_exit 处下断点看下 vcpu 的调度情况
发现 vcpu 调度是正常的

但在 kvm_riscv_vcpu_exit 处打 log 发现每次 guest 退出的原因都是因为
sbi 设置 timecmp

通过 qemu 的 command dump-guest-memory 命令导出 guest 的 coredump
guest 停在了通过 sbi 设置 timecmp 的地方.

根据这个现象, guest kernel 一直在处理 timer tick, 却不能走其他的流程, 对比正常的 log, 启动过程中从 guest 退出到 kvm 的原因是多种多样的, 所以可以首先怀疑是 guest timer tick 出了问题.

对比最新版本 qemu (8.0) 搭配 linux kernel (主线 6.x) 版本, 可以正常运行 guest.
对比 linux kernel kvm 中对 guest timer tick 的处理, 发现加入了 sstc 的 feature.

初步验证

sstc 可以作为第一个疑点, 需要首先排除下是否是这个 feature 导致的.

在 host qemu (8.0) 中关掉 sstc 的 feature, 即关掉模拟 riscv 主机的 sstc 的 feature, 再次运行 guest, 发现 guest 正常运行了.

调研 sstc 的 SPEC 文档, sstc 引入了 vstimecmp 寄存器, guest 可以通过直接设置 vstimecmp 来避免陷入 kvm 来处理 timer tick.

但调研 SPEC 结束后, 发现不能解释为什么 old kernel (5.19.16) kvm 上不使用 vstimecmp 应该是没问题的, guest 通过 sbi 设置 timecmp 是走了通过 host kvm 来处理 guest timer tick 的逻辑.

guest os 设置 compare 发 SBI_EXT_0_1_SET_TIMER 的请求
导致陷入到 kvm 中, kvm 接收的请求中第一个参数 next_cycle 即为下一轮的 compare 数.

在 guest 因设置 compare 请求导致陷入到 kvm 中, kvm 处理 SBI_EXT_0_1_SET_TIMER:

  • 首先清除了 IRQ_VS_TIMER 中断, 确保本轮 tick 中不再有有的 timer 中断产生
  • 计算下一轮到期的时间 delta_ns (cycle 转换为 host 的纳秒单位)
  • 启动定时器, 到期时间为当前时间+delta_ns
  • 退出 kvm, 重返到 vcpu guest

定时器到期时, 触发中断重新陷入到 kvm 中, 处理函数为 kvm_riscv_vcpu_hrtimer_expired
, 通过 hvip 注入 IRQ_VS_TIMER 中断, 让 guest 处理 timer tick, 走到对应的 tick 处理函数, 紧接着通过 sbi SBI_EXT_0_1_SET_TIMER 设置下一轮 compare.

这套机制的问题只是效率慢 (一次 timer tick 的处理需要两次陷入 kvm), 但机制是没问题的.

再次深入调研 sstc 的 SPEC, 注意到这个描述:

The interrupt remains posted until vstimecmp becomes
Greater than (time + htimedelta) - typically as a result of writing vstimecmp

会不会是 vstimecmp 没处理是默认的 0, 所以 compare 总是比 time 小, 导致一直触发 VSTIP 的中断, guest 一直在处理 VSTIP 中断, 不能让出 cpu 来处理其他的流程.

想到这一点, 可以简单的验证下:

kvm_arch_vcpu_load 中设置 vstimecmp 为最大值
csr_write(0x24D, -1UL);

修改后验证 qemu (8.0)+old kernel (5.19.17) 模拟的 riscv 主机上跑 guest, guest 正常运行了, 证明正是这个问题导致的.

梳理 guest timer 的流程

在找到 root cause 后, 有时间需要对 guest riscv timer tick 的流程做一下梳理.
这里直接拿最新的 linux kernel 版本进行分析.

首先整理 sstc 的 feature.

guest 在满足上述 [m/h]envcfg.STCE[m/h]counteren.TM 后可以直接操作 stimecmp csr 来设置 timer tick, 就不必再通过 sbi 陷入到 kvm 中设置 compare, 也不需要 kvm 维护定时器, 同时也不需要 kvm 注入 timer 虚拟中断给 vcpu, 减少了两次陷入 kvm, 大大提升了 guest 处理 tick 的效率.
Tick 到期后, 由硬件来保证 vcpu 直接收到 timer 的虚拟中断, vcpu 不用陷入到 kvm 就可以处理一轮轮的 tick.

为什么上述问题中, guest os 却是设置 compare 发 SBI_EXT_0_1_SET_TIMER 的请求
这是因为这个问题中, guest kernel 用的 old kernel (5.19.17), 并未合入 sstc feature 的相关修改.

可以直接看下 guest kernel 处理 timer tick 的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
start_kernel
+-> time_init
+-> timer_probe
+-> cpuhp_setup_state
+-> __cpuhp_setup_state
+-> __cpuhp_setup_state_cpuslocked
+-> cpuhp_issue_call
+-> cpuhp_invoke_callback
+-> riscv_timer_starting_cpu
+-> clockevents_config_and_register
+-> clockevents_register_device
+-> tick_check_new_device
+-> tick_setup_device
+-> tick_setup_periodic
/* event_handler回调 */
+-> tick_handle_periodic
+-> clockevents_program_event
+-> dev->set_next_event
+-> riscv_clock_next_event

old kernel (5.19.17)

1
2
3
4
5
6
7
static int riscv_clock_next_event(unsigned long delta,
struct clock_event_device *ce)
{
csr_set(CSR_IE, IE_TIE);
sbi_set_timer(get_cycles64() + delta);
return 0;
}

而最新主线上的 kernel (6.x)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static int riscv_clock_next_event(unsigned long delta,
struct clock_event_device *ce)
{
u64 next_tval = get_cycles64() + delta;

csr_set(CSR_IE, IE_TIE);
if (static_branch_likely(&riscv_sstc_available)) {
...
csr_write(CSR_STIMECMP, next_tval);
} else
sbi_set_timer(next_tval);

return 0;
}

guest kernel 未合入 sstc feature 修改前, guest 只能通过 sbi SBI_EXT_0_1_SET_TIMER 设置 compare.
需要陷入到 kvm 中设置 compare.

guest kernel 合入了 sstc feature 后, guest kernel 可以检测主机是否支持 sstc,

  • 如支持, guest 通过 stimecmp (vstimecmp) 来设置 compare, 最大化的提高 timer tick 的处理效率.
  • 不支持, guest 仍通过 sbi SBI_EXT_0_1_SET_TIMER 设置 compare.

而最新的 linux kvm 版本合入了 sstc 的 feature, 则需要考虑 guest kernel 的兼容性, 同时兼容老的 kernel (不支持 sstc) 和新的 kernel(支持 sstc)

来看下对应的处理逻辑:

  • 模拟的 riscv 主机支持 sstc 时:
    1. 对于支持 sstc 的新的 guest kernel, 在 guest 运行后, 就不用管 guest 的 timer tick 了, 但是 guest 退出运行后, 处于 block 时, 仍然需要关注 guest 的 timer tick.
    2. 对于不支持 sstc 的旧的 guest kernel, 需要处理 guest 发过来的 sbi SBI_EXT_0_1_SET_TIMER 消息. 但 guest 运行时, 也不需要通过主机定时器来处理 guest 的 timer tick. 因为 host 是支持 sstc 的, 只是 guest kernel 未检测 sstc 而已. 只要 kvm 在收到 sbi SBI_EXT_0_1_SET_TIMER 后直接设置 vstimecmp 就可以了, 这样时间到了后, 硬件会设置 VSTIP, guest 就可以处理 timer tick 中断走到 tick 处理函数了.
  • 模拟的 riscv 主机不支持 sstc 时:
    1. 退回到通过 host kvm 设置定时器来处理 guest 的 timer tick.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int kvm_riscv_vcpu_timer_init(struct kvm_vcpu *vcpu)
{
struct kvm_vcpu_timer *t = &vcpu->arch.timer;

if (t->init_done)
return -EINVAL;

hrtimer_init(&t->hrt, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
t->init_done = true;
t->next_set = false;

/* Enable sstc for every vcpu if available in hardware */
if (riscv_isa_extension_available(NULL, SSTC)) {
t->sstc_enabled = true;
t->hrt.function = kvm_riscv_vcpu_vstimer_expired;
t->timer_next_event = kvm_riscv_vcpu_update_vstimecmp;
} else {
t->sstc_enabled = false;
t->hrt.function = kvm_riscv_vcpu_hrtimer_expired;
t->timer_next_event = kvm_riscv_vcpu_update_hrtimer;
}

return 0;
}

riscv 主机支持 sstc

看下 riscv 主机支持 sstc 的策略:
t->timer_next_event = kvm_riscv_vcpu_update_vstimecmp;

该函数是在处理 guest 发来的 sbi SBI_EXT_0_1_SET_TIMER 的处理函数

1
2
3
4
5
6
7
-+ kvm_sbi_ext_v01_handler
\ -+ case SBI_EXT_0_1_SET_TIMER:
\ - next_cycle = (u64)cp->a0;
| -+ kvm_riscv_vcpu_timer_next_event(vcpu, next_cycle);
\ -+ t->timer_next_event(vcpu, ncycles);
\ -+ kvm_riscv_vcpu_update_vstimecmp()
\ - csr_write(CSR_VSTIMECMP, ncycles);

对应支持 sstc 模拟 riscv 主机的第二种情况.
而第一种情况 guest 运行中 guest kernel 自己设置 vstimecmp, 不需要陷入 kvm, 所以 kvm 也不用处理.

再看下
t->hrt.function = kvm_riscv_vcpu_vstimer_expired;

这个是 host kvm 定时器的超期函数, 这个函数是为了处理 guest kernel 因 block 得不到执行时的定时器处理函数, 该定时器是类似于 doorbell 机制, 在 guest kernel 未能调度时, 需要 kvm 设置定时器处理 guest 的 tick. 保证 vcpu 可以有调度的机会.

1
2
3
4
5
6
7
8
9
10
11
12
13
"退出ioctl(KVM_RUN)的小循环时, 调用了 vcpu_put 释放vcpu资源"
-+ kvm_arch_vcpu_ioctl_run(vcpu)
\ -+ vcpu_put(vcpu)
\ -+ kvm_arch_vcpu_put(vcpu);
\ -+ kvm_riscv_vcpu_timer_save(vcpu);
\ -|+ if kvm_vcpu_is_blocking(vcpu)
\ -+ kvm_riscv_vcpu_timer_blocking(vcpu);
"启动定时器"
\ - hrtimer_start(&t->hrt, ktime_set(0, delta_ns), HRTIMER_MODE_REL);
"定时器到时处理函数"
-+ kvm_riscv_vcpu_vstimer_expired(struct hrtimer *h)
\ - kvm_vcpu_kick(vcpu); "唤醒vcpu"
| - return HRTIMER_NORESTART; "不重启定时器"

riscv 主机不支持 sstc

回退 kvm 主机为 guest timer tick 设置定时器
t->timer_next_event = kvm_riscv_vcpu_update_hrtimer;

在 guest 因设置 compare 请求导致陷入到 kvm 中, kvm 处理 SBI_EXT_0_1_SET_TIMER:
kvm_riscv_vcpu_update_hrtimer

  • 首先清除了 IRQ_VS_TIMER 中断, 确保本轮 tick 中不再有新的 timer 中断产生
  • 计算下一轮到期的时间 delta_ns (cycle 转换为 host 的纳秒单位)
  • 启动定时器, 到期时间为当前时间+delta_ns
  • 退出 kvm, 重返到 vcpu guest

定时器到期时, 触发中断重新陷入到 kvm 中, 处理函数为 kvm_riscv_vcpu_hrtimer_expired
, 通过 hvip 注入 IRQ_VS_TIMER 中断, 让 guest 处理 timer tick, 走到对应的 tick 处理函数, 紧接着通过 sbi SBI_EXT_0_1_SET_TIMER 设置下一轮 compare.

内存虚拟化

涉及到两个主题:

  1. GPA->HVA
  2. HVA->HPA

略过 GVA->GPA, 这个是 guest os 中的 mmu 地址翻译过程.

GPA->HVA

KVM-Qemu 方案中,GPA->HVA 的转换,是通过 ioctl 中的 KVM_SET_USER_MEMORY_REGION 命令来实现的.

数据结构

  • 虚拟机使用slot来组织物理内存,每个slot对应一个struct kvm_memory_slot,一个虚拟机的所有slot构成了它的物理地址空间;
  • 用户态使用struct kvm_userspace_memory_region来设置内存slot,在内核中使用struct kvm_memslots结构来将kvm_memory_slot组织起来;
  • struct kvm_userspace_memory_region结构体中,包含了slot的 ID 号用于查找对应的slot,此外还包含了物理内存起始地址及大小,以及 HVA 地址,HVA 地址是在用户进程地址空间中分配的,也就是 Qemu 进程地址空间中的一段区域;

流程分析

数据结构部分已经罗列了大体的关系,那么在KVM_SET_USER_MEMORY_REGION时,围绕的操作就是slots的创建、删除,更新等操作

  • 当用户要设置内存区域时,最终会调用到__kvm_set_memory_region函数,在该函数中完成所有的逻辑处理;
  • __kvm_set_memory_region函数,首先会对传入的struct kvm_userspace_memory_region的各个字段进行合法性检测判断,主要是包括了地址的对齐,范围的检测等;
  • 如果传入的参数中memory_size为 0,那么会将对应slot进行删除操作;
  • 根据用户传入的参数,设置slot的处理方式:KVM_MR_CREATEKVM_MR_MOVEKVM_MEM_READONLY
  • 根据用户传递的参数决定是否需要分配脏页的 bitmap,标识页是否可用;
  • 最终调用kvm_set_memslot来设置和更新slot信息;

kvm_set_memslot

具体的memslot的设置在kvm_set_memslot函数中完成,slot的操作流程如下:

  • 首先分配一个新的memslots,并将原来的memslots内容复制到新的memslots中;
  • 如果针对slot的操作是删除或者移动,首先根据旧的slot id号从memslots中找到原来的slot,将该slot设置成不可用状态,再将memslots安装回去。这个安装的意思,就是 RCU 的 assignment 操作,不理解这个的,建议去看看之前的 RCU 系列文章。由于slot不可用了,需要解除 stage2 的映射;
  • kvm_arch_prepare_memory_region函数,用于处理新的slot可能跨越多个用户进程 VMA 区域的问题,如果为设备区域,还需要将该区域映射到Guest GPA中;
  • update_memslots用于更新整个memslotsmemslots基于 PFN 来进行排序的,添加、删除、移动等操作都是基于这个条件。由于都是有序的,因此可以选择二分法来进行查找操作;
  • 将添加新的slot后的memslots安装回 KVM 中;
  • kvfree用于将原来的memslots释放掉;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
-+ kvm_set_memslot(struct kvm *kvm, struct kvm_memory_slot *old, struct kvm_memory_slot *new, enum kvm_mr_change change)
\ -+ kvm_invalidate_memslot(kvm, old, invalid_slot);
"KVM_MR_DELETE 或 KVM_MR_MOVE 情况下需要无效化该slot以告知vcpu 该slot映射已经不存在了, 如果该内存插槽在KVM虚拟机中正在使用,则此函数将导致任何对该内存区域的访问引发异常。"
\ -+ kvm_arch_flush_shadow_memslot(kvm, old);
\ -+ gstage_unmap_range(kvm, gpa, size, false); "riscv 解除stage2 映射"
\ -+ gstage_op_pte(kvm, addr, ptep, ptep_level, GSTAGE_OP_CLEAR);
\ - set_pte(ptep, __pte(0)); "G-stage pte 清除, 进而导致vcpu 触发page fault"
| -+ kvm_prepare_memory_region(kvm, old, new, change);
\ -+ kvm_arch_prepare_memory_region(kvm, old, new, change); "riscv 实现"
"change 为 KVM_MR_CREATE 或 KVM_MR_MOVE 或 KVM_MR_FLAGS_ONLY 情况时"
\ - hva = new->userspace_addr;
| - size = new->npages << PAGE_SHIFT; "新申请的memslot range "
| - reg_end = hva + size;
| - base_gpa = new->base_gfn << PAGE_SHIFT;
"新申请的memslot 内存区域可能跨多个 VMA, 所以需要遍历所有的VMA, 查看 vma 是否覆盖了 hva的地址范围"
| -+ while (vma = find_vma(current->mm, hva)) "查到的vma 包含了hva地址, current->mm 代表查找当前进程即qemu的vma"
\ - pa = (phys_addr_t)vma->vm_pgoff << PAGE_SHIFT;
| - vm_start = max(hva, vma->vm_start);
| - pa += vm_start - vma->vm_start; "pa 为 HPA"
| - gpa = base_gpa + (vm_start - hva);
| -|+ if (vma->vm_flags & VM_PFNMAP) "预先映射 gpa->hpa 页表的前提是触及的vma 有 VM_PFNMAP 的flag"
\ -+ gstage_ioremap(kvm, gpa, pa, size(vm_end - vm_start), writable);
"维护gpa -> hpa映射关系"
\ - end = (gpa + size + PAGE_SIZE - 1) & PAGE_MASK;
| - pfn = __phys_to_pfn(hpa); "hpa 物理页号"
| -+ for (addr = gpa; addr < end; addr += PAGE_SIZE)
\ - pte = pfn_pte(pfn, PAGE_KERNEL);
| - kvm_mmu_topup_memory_cache(&pcache, gstage_pgd_levels); "预分配mmu 页表本身所需的内存"
| -+ gstage_set_pte(kvm, level:0, &pcache, GPA:addr, &pte);
\ - current_level = gstage_pgd_levels - 1; "这里只看到 sv39x4, gstage_pgd_levels=3"
| - pte_t *next_ptep = (pte_t *)kvm->arch.pgd; "kvm 为虚拟机的实例, pgd 为G-stage 第一级页表基址"
| - pte_t *ptep = &next_ptep[gstage_pte_index(addr, current_level)]; "取出第一级页表项"
| -+ while (current_level != level(0))
\ - gstage_pte_leaf(ptep) "检查不应为叶子节点"
| -|+ if(!pte_val(*ptep)) "下一级页表项无效, 需要创建下一级页表项"
"从预分配内存中找出一个可用的内存地址放下一级页表项"
\ - next_ptep = kvm_mmu_memory_cache_alloc(pcache);
| - *ptep = pfn_pte(PFN_DOWN(__pa(next_ptep)), __pgprot(_PAGE_TABLE));
"更新当前页表项, 保存下一级页表项 ppn 及 权限"
-|+ else "下一级页表项有效"
\ - next_ptep = (pte_t *)gstage_pte_page_vaddr(*ptep); "直接找出下一级页表项地址"
| - current_level--; "处理下一级页表项"
| - ptep = &next_ptep[gstage_pte_index(addr, current_level)]; "ptep 更新为下一级页表项地址"
| - *ptep = *pte; "最终更新叶子节点为 HPA的pte"
| -+ gstage_remote_tlb_flush(kvm, current_level, addr); "G-stage 页表更新后, 还需要刷新tlb"\
\ -+ kvm_riscv_hfence_gvma_vmid_gpa(kvm, -1UL, 0, addr, BIT(order), order);
\ ---+ kvm_riscv_hfence_process(vcpu)
\ -+ case KVM_RISCV_HFENCE_GVMA_VMID_GPA:
\ - kvm_riscv_local_hfence_gvma_vmid_gpa(READ_ONCE(v->vmid), d.addr, d.size, d.order);
| - hva = vm_end; "更新hva"
| -+ kvm_create_memslot(kvm, new); "if KVM_MR_CREATE"
| -+ kvm_delete_memslot(kvm, old, invalid_slot); "if KVM_MR_DELETE"
| -+ kvm_move_memslot(kvm, old, new, invalid_slot); "if KVM_MR_MOVE"
| -+ kvm_update_flags_memslot(kvm, old, new); "if KVM_MR_FLAGS_ONLY"
| -+ kvm_commit_memory_region(kvm, old, new, change);

在 riscv 中, kvm_arch_prepare_memory_region 的实现判断如果vma 有 VM_PFNMAP 的flag, 会调用 gstage_ioremap 进而调用 gstage_set_pte 建立 GPA->HPA的G-stage 页表

最终建立页表映射关系的函数为 gstage_set_pte.
而不是所有的vma 都有这个flag, 大部分的 GPA->HPA的 G-stage 页表映射的建立还是在触发Guest Page Fault 时处理的.

kvm_commit_memory_region

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
-+ kvm_commit_memory_region(struct kvm *kvm,
struct kvm_memory_slot *old,
const struct kvm_memory_slot *new,
enum kvm_mr_change change)
\ - kvm->nr_memslot_pages -= old->npages; "if KVM_MR_DELETE"
| - kvm->nr_memslot_pages += new->npages; "if KVM_MR_CREATE"
| -+ kvm_arch_commit_memory_region(kvm, old, new, change);
\ -|+ if(change != KVM_MR_DELETE && new->flags & KVM_MEM_LOG_DIRTY_PAGES)
\ -+ gstage_wp_memory_region(kvm, new->id);
\ - kvm_memslots *slots = kvm_memslots(kvm); "找到memslots"
| - kvm_memory_slot *memslot = id_to_memslot(slots, slot); "根据slot id 找到对应的slot"
| - start = memslot->base_gfn << PAGE_SHIFT;
| - end = (memslot->base_gfn + memslot->npages) << PAGE_SHIFT;
| -+ gstage_wp_range(kvm, start, end);
\ -+ while (addr < end)
\ -+ gstage_op_pte(kvm, addr, ptep, ptep_level, GSTAGE_OP_WP);
\ - set_pte(ptep, __pte(pte_val(*ptep) & ~_PAGE_WRITE)); "操作G-stage 页表pte, 去除W权限, write protector"
| - kvm_flush_remote_tlbs(kvm);

GPA->HPA

gstage_page_fault 根据guest page fault 时的htval/stval 记录的fault addr 为 GPA, 通过GPA 在memslots中找到对应的memslot, 进而找到 hva 和 hpa, 根据hva所在的vma 的page_size 确定是建立大页还是普通的G-stage 页表
, 最终通过 gstage_set_pte 函数建立G-stage 页表.
中途还处理了hpa的dirty 和 memslot 中的dirty_bitmap 以及 G-stage 页表pte的dirty位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
-+ gstage_page_fault(struct kvm_vcpu *vcpu, struct kvm_run *run,
struct kvm_cpu_trap *trap)
\ - fault_addr = (trap->htval << 2) | (trap->stval & 0x3); "guest page fault 时 通过htval 或 stval 记录 GPA"
| - gfn = fault_addr >> PAGE_SHIFT; "GPA的页号"
| - memslot = gfn_to_memslot(vcpu->kvm, gfn); "通过GPA 页号找到kvm对应的memslot"
| - hva = gfn_to_hva_memslot_prot(memslot, gfn, &writeable); "通过memslot gpa页号找到 hva"
"找到了hva, 说明是内存, 不会陷入到 emulate_load emulate_store io模拟"
| -+ kvm_riscv_gstage_map(vcpu, memslot, GPA:fault_addr, hva,
is_write:(trap->scause == EXC_STORE_GUEST_PAGE_FAULT) ? true : false);
\ - vma = find_vma_intersection(current->mm, hva, hva + 1); "查找vma"
| - kvm_mmu_topup_memory_cache(pcache, gstage_pgd_levels); "预分配页表本身所需内存"
| -+ hfn = gfn_to_pfn_prot(kvm, gfn, is_write, &writeable); "gpa页号找到hpa页号"
\ - addr = __gfn_to_hva_many(slot, gfn, NULL, write_fault); "先通过gpa 找到hva"
| - return hva_to_pfn(addr, atomic, async, write_fault, writable); "再通过hva 找到hpa 得到hfn"
| -|+ if (writeable)
\ - kvm_set_pfn_dirty(hfn); "hfn pte 置dirty"
| - mark_page_dirty(kvm, gfn); "对应memslot的 dirty_bitmap 置位, 维护虚拟机状态"
| -+ gstage_map_page(kvm, pcache, gpa, hfn << PAGE_SHIFT,
page_size:vma_pagesize, page_rdonly:false, page_exec:true);
"根据page_size 确定建几级页表, 比如该vma的page_size 很大, 超过了 1个page, 就可以考虑建大页"
\ - gstage_page_size_to_level(page_size, &level);
| - new_pte = pfn_pte(PFN_DOWN(hpa), prot); "更新pte 状态, prot 根据 page_rdonly page_exec 确定 RWX权限"
| - new_pte = pte_mkdirty(new_pte); "pte 添加 Dirty状态"
| - gstage_set_pte(kvm, level, pcache, gpa, &new_pte); "建立 G-stage GPA->HPA 页表"
| -|+ else
\ - ret = gstage_map_page(kvm, pcache, gpa, hfn << PAGE_SHIFT,
vma_pagesize, true, true);

cpu 虚拟化

vcpu 调用流

梳理下vcpu 调度的流程, 需要结合下qemu 来看下

KVM_VCPU_RUN

qemu 用户态代码, qemu 为每个vcpu 建了一个新的线程, 专门用来运行vcpu, 主要逻辑就是一个while 循环调用ioctl(KVM_RUN), 如果vcpu guest 一直运行, 则该次ioctl 不会结束,

  • 只有当guest 因请求导致异常陷入kvm 后, kvm 判断需要用户态处理该请求时, 才会结束这次ioctl的调用, 回到qemu 中
  • qemu 接着判断这次需要用户态处理的原因, 处理完后, 再进行下一轮while 循环, 继续发送ioctl(KVM_RUN)
  • qemu 将本次处理的结果在下一轮循环中告诉kvm, kvm 在处理完qemu处理的结果后, 再进到guest, 结束本次guest的请求.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
while(1) {
ret = ioctl(vcpufd, KVM_RUN, NULL);
switch (run->exit_reason)
case KVM_EXIT_IO:
kvm_handle_io(run->io.port, attrs,
(uint8_t *)run + run->io.data_offset,
run->io.direction,
run->io.size,
run->io.count);
...
case KVM_EXIT_MMIO:
address_space_rw(&address_space_memory,
run->mmio.phys_addr, attrs,
run->mmio.data,
run->mmio.len,
run->mmio.is_write);
...
}

kvm 中vcpu run的流程(精简)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
```erlang
-+ kvm_vcpu_ioctl(struct file *filp, unsigned int ioctl, unsigned long arg)
\ - kvm_arch_vcpu_async_ioctl(filp, ioctl, arg); "riscv not implement"
| -+ case KVM_RUN "ioctl 值"
\ - oldpid = rcu_access_pointer(vcpu->pid); "保存vcpu->pid 值, 这个值表示用户态qemu 创建的vcpu 线程id"
| -|+ if oldpid != task_pid(current) "线程id不一样"
\ -+ kvm_arch_vcpu_run_pid_change(vcpu) "riscv not implement"
| - newpid = get_task_pid(current, PIDTYPE_PID);
| - rcu_assign_pointer(vcpu->pid, newpid); "vcpu 指向新的线程id"
| -+ r = kvm_arch_vcpu_ioctl_run(vcpu);
\ - kvm_run *run = vcpu->run;
| - vcpu->arch.ran_atleast_once = true; "标记运行过"
| -+ switch (run->exit_reason)
\ -|+ case KVM_EXIT_MMIO: "处理寄存器"
"比如guest 此时 [ld t0, 地址], 此处为 qemu 读到的mmio 地址的值, 最后要调用这个函数将值塞到 guest_context.t0中"
\ - kvm_riscv_vcpu_mmio_return(vcpu, vcpu->run);
| -|+ case KVM_EXIT_RISCV_SBI:
\ - kvm_riscv_vcpu_sbi_return(vcpu, vcpu->run); "Process SBI value returned from user-space"
| -|+ case KVM_EXIT_RISCV_CSR:
\ - kvm_riscv_vcpu_csr_return(vcpu, vcpu->run); "Process CSR value returned from user-space"
| -+ vcpu_load(vcpu);
\ -+ kvm_arch_vcpu_load(vcpu, cpu);
\ - kvm_vcpu_csr *csr = &vcpu->arch.guest_csr; "guest_csr 在前面KVM_VCPU_CREATE 创建时已经初始化过了"
| - csr_write(CSR_VSEPC, csr->vsepc); "读取vcpu->guest_csr 中的v开头的csr, 恢复到 cpu的对应csr中"
| -+ while(ret > 0)
\ - kvm_riscv_gstage_vmid_update(vcpu); "更新vmid"
| - kvm_riscv_check_vcpu_requests(vcpu);
| - local_irq_disable(); "关闭 hs kernel的中断"
| - kvm_riscv_vcpu_flush_interrupts(vcpu); "根据qemu 注入的 irqs_pending sync vcpu->guest_csr 的hvip"
| - kvm_riscv_update_hvip(vcpu); "根据vcpu->guest_csr的hvip 更新 cpu的 hvip csr"
| -+ kvm_riscv_vcpu_enter_exit(vcpu); "切换vcpu 虚拟机运行"
\ -+ __kvm_riscv_switch_to(&vcpu->arch); "进入 guest"
"同时设置stvec 为 __kvm_switch_return, 这样因异常或中断退出guest 返回host 时, 会走到 __kvm_switch_return 中"
-----------> "因异常或中断退出guest, 回到kvm上下文, 退出guest 切换到 host 时, 会保存guest 的上下文到 guest_context 中" <--------
| ---+ __kvm_switch_return "保存guest 上下文, 恢复host 上下文, 恢复stvec 为 hs kernel 原本的stvec"
| -
| - trap.sepc = vcpu->arch.guest_context.sepc;
| - trap.htval = csr_read(CSR_HTVAL);
| - trap.scause = csr_read(CSR_SCAUSE);
| - kvm_riscv_vcpu_sync_interrupts(vcpu); "guest 有可能会操作 sip.VSSIP 导致 hvip.VSSIP 变化了, 这里要跟host 中的hvip 变量同步一下"
| - local_irq_enable(); "开启hs kernel 的中断, 如果有hs kernel 的异常/中断的pending, 会在这个地方进入到hs kernel的异常/中断处理流程中"
| -+ kvm_riscv_vcpu_exit(vcpu, run, &trap); "判断异常的原因, 对异常进行处理"
\ - switch (trap->scause)
| -|+ case EXC_INST_ILLEGAL
| -|+ case EXC_VIRTUAL_INST_FAULT
| -|+ case EXC_INST_GUEST_PAGE_FAULT | EXC_LOAD_GUEST_PAGE_FAULT | EXC_STORE_GUEST_PAGE_FAULT
\ -+ gstage_page_fault(vcpu, run, trap);
\ - switch (trap->scause)
| -|+ case EXC_LOAD_GUEST_PAGE_FAULT
\ -+ return kvm_riscv_vcpu_mmio_load(vcpu, run, fault_addr, trap->htinst);
\ -|+ if (!kvm_io_bus_read(vcpu, KVM_MMIO_BUS, fault_addr, len, data_buf) "如果kvm能自己处理, 则不会陷入用户态, 由kvm_io 设备处理"
\ - kvm_riscv_vcpu_mmio_return(vcpu, run); "处理寄存器"
"比如guest 此时 [ld t0, 地址], kvm从kvm_io设备中读出该地址的值, 最后要调用这个函数将值塞到 guest_context.t0中"
| <== ret = 1 \ - return 1 " 不需要陷入用户态, ret 返回1, while继续, 下一轮重新进入guest 中"
| -|+ else
\ - vcpu->stat.mmio_exit_user++;
| - run->exit_reason = KVM_EXIT_MMIO; "陷入用户态 qemu, qemu需要知道原因"
| <== ret = 0 | - return 0; "由用户态 qemu 进行处理, 这个地方return 0, 退出while 循环, 本轮 ioctl(vcpufd, KVM_RUN, NULL) 调用结束, 回到qemu用户态"
| -|+ case EXC_STORE_GUEST_PAGE_FAULT
| <== \ -+ return kvm_riscv_vcpu_mmio_store(vcpu, run, fault_addr, trap->htinst);
| - kvm_riscv_gstage_map(vcpu, memslot, fault_addr, hva, is_write) "如果能通过gpa 找到hva, 对应ram 的地址, 则直接建立G-stage 页表就行了"
| <== | - return 1; " 继续while 循环, 下一轮进入guest"
| -|+ case EXC_SUPERVISOR_SYSCALL
\ -+ kvm_riscv_vcpu_sbi_ecall(vcpu, run);
| -+ vcpu_put(vcpu); "上面return 0 时代表需要进入qemu 中进行处理, 继续走到这里"
\ -+ kvm_arch_vcpu_put(vcpu); "主要是读取v 开头的csr, 保存到vcpu->guest_csr 下"
| - "结束ioctl 调用"

为了更容易理解, 只对mmio load 这一种情况做一下简要的流程介绍:

图中两个循环, 一个是qemu 中的 (黄色线) , 一个是kvm中的 (红色线)
当guest 因load 指令陷入到 kvm 中时, 查找GPA 没找到 关联的hva (说明是MMIO 地址, 不是RAM 地址)
如果kvm 可以自己处理, 即通过遍历 KVM_MMIO_BUS 上注册的kvm_io_dev 设备的地址范围, 查找gpa 是否在该范围内

  • 如果在 KVM_MMIO_BUS 地址范围, 则由kvm_io_dev的对应的read 函数进行处理
  • 如果不在, 则需要退出kvm的 KVM_RUN 的循环, 结束ioctl(KVM_RUN), 回到qemu 用户态, 原因为 KVM_EXIT_MMIO, 由qemu 在AddressSpace 查找对应的MemoryRegion, 调用MemoryRegion的read函数进行处理, 处理完后, qemu 进行下一轮循环, 重新发起 ioctl(KVM_RUN) 进入kvm
  • 上述两种方式读到了mmio 地址 (GPA) 的值, 最终都需要调用 kvm_riscv_vcpu_mmio_return 将该地址的值填到对应的 guest 发起的load 目标寄存器中.
  • 最后通过 kvm_riscv_vcpu_enter_exit 函数重新进入guest 中.

注意host 上下文和 guest 上下文的保存恢复过程, 上述为了简述只写了通用寄存器的保存恢复流程, 实际还有一些cpu的 csr 需要保存恢复. 如 host的 scounteren sstatus hstatus hvip hgatp 等 (一个物理cpu可以托管多个vcpu, 而每个vcpu的这几个csr可能都是不同的)
guest 的主要的v开头的csr 属于是vcpu 专属的, 这部分放在了vcpu->guest_csr 中, 需要在 vcpu_put 和 vcpu_load 中进行保存和恢复. 这一部分为什么没在 kvm_riscv_vcpu_enter_exit 时进行处理是因为, kvm的 KVM_RUN的循环是针对同一个vcpuid的, 离开了这个循环才有可能在本物理cpu上调度其他vcpuid的vcpu, 将v 开头的csr保存恢复放在 KVM_RUN 循环外避免了 无意义的保存恢复.

vCPU 调度

首先需要知道 qemu-kvm 框架下vcpu 和 虚拟机实例 vm 的角色.

KVM中的一个客户机(VM 实例)是作为一个用户空间进程(qemu-kvm)运行。和其他普通的用户空间进程一样,由内核调度。
多个客户机(VM 实例)就是宿主机中的多个QEMU进程
而一个客户机的多个vCPU就是一个QEMU进程中的多个线程。

因为vCPU实际上是宿主机上的一个线程,所以vCPU的调度是依赖宿主机Linux内核中的CPU调度算法。
即kvm 中并没有为vcpu 做专门的调度.

宿主机调度时机基于宿主机的每一次的tick处理函数, 根据运行的时间片或负载调整内核task的切换.
用户态的进程和线程落到kernel 中都是task.
宿主机因tick 中断会从vcpu guest 中退出来陷入到 host kernel 中的tick 处理函数中.
host kernel 根据对应的调度策略选择切换到其他的task.

Linux调度算法有三个比较著名:

  1. O(1) scheduler
  2. Comletely Fair Scheduler(CFS)
  3. BF Scheduler(BFS)

对smp的支持

在多个hart 上并行执行多个vcpu的task.
vcpu task 因为是属于qemu的线程, 所以vcpu 可以更换物理hart运行.
linux 的CFS算法会根据smp负载情况调度qemu的线程, 使vcpu 运行在负载较低的物理hart上.

hs timer tick 调度细节

从源码上看, kvm 在进guest 之前, 将stvec 变成了kvm的 __kvm_switch_return
在vcpu 正在运行时, 如果因为 hs kernel的timer tick 到了, hs kernel 会因中断打断vcpu guest 的运行, 进而会先走到 __kvm_switch_return
__kvm_switch_return 会保存guest上下文/恢复host的上下文, 将stvec 恢复成hs kernel 原本的stvec, 接着处理kvm循环的后半部分,
接着会调用 local_irq_enable 这个函数会启用hs kernel 的中断, 开启后因为 sip & sie timer的pending, 所以会接着走timer的tick 的处理函数处理该pending, 也就进入了cpu调度的部分. cpu可以根据负载情况从 vcpu run的task 切换到别的task上.

但是注意此时vcpu run的task 还是在这个物理hart上, 从源码上看不允许将该task 切换到别的物理hart上执行, kvm的 KVM_RUN的循环没有退出, vcpu的资源也没有释放, vcpu在kvm中的相关资源(timer 中断, 更复杂的场景如支持中断直通)没有切换物理hart的时机, vcpu->cpu 并没有改变的时机, 只有KVM_RUN 结束后回到qemu的循环时, vcpu的资源才会释放, 该vcpu才能跑到其他的物理hart上.
这个可能会导致vcpu 长期绑到了一个物理hart上.

timer 虚拟化

从操作系统的角度来看一下 timer 的作用吧:

通过 timer 的中断,OS 实现的功能包括但不局限于上图:

  • 定时器的维护,包括用户态和内核态,当指定时间段过去后触发事件操作,比如 IO 操作注册的超时定时器等;
  • 更新系统的运行时间、wall time 等,此外还保存当前的时间和日期,以便能通过time()等接口返回给用户程序,内核中也可以利用其作为文件和网络包的时间戳;
  • 调度器在调度任务分配给 CPU 时,也会去对 task 的运行时间进行统计计算,比如 CFS 调度,Round-Robin 调度等;
  • 资源使用统计,比如系统负载的记录等,此外用户使用 top 命令也能进行查看;

riscv 中 对timer的虚拟化支持

  • mtime csr 时间来自于clint, 所有cpu 共享, 统一维护一个时间基准.
  • htimedelta, 每个cpu 有一个htimedelta csr, 各自记录与mtime 之间的offset.
  • time csr, 在V=1时, time = mtime + htimedelta; 在kvm中(HS-mode下), time = mtime

示例如下:

6ms 的时间段里,每个 vCPU 运行 3ms,Hypervisor 可以使用 htimedelta 寄存器来将 vCPU 的时间调整为其实际的运行时间;

用户层访问

可以从用户态对 vtimer 进行读写操作,比如 Qemu 中,流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
---> qemu <---
-+ ioctl(vcpu_fd, KVM_GET_ONE_REG, 0)
---> kernel <---
\ -+ kvm_arch_vcpu_ioctl
\ -+ case KVM_GET_ONE_REG
\ -+ kvm_riscv_vcpu_get_reg(vcpu, &reg);
\ -+ kvm_riscv_vcpu_get_reg_timer
\ -+ case KVM_REG_RISCV_TIMER_REG(time)
\ -+ reg_val = kvm_riscv_current_cycles(gt);
\ - csr_read(CSR_TIME) + gt->time_delta;
| -+ case KVM_REG_RISCV_TIMER_REG(compare)
\ - reg_val = t->next_cycles;
  • 用户态创建完 vcpu 后,可以通过 vcpu 的文件描述符来进行寄存器的读写操作;
  • ioctl 通过KVM_SET_ONE_REG/KVM_GET_ONE_REG将最终触发寄存器的读写;
  • 如果操作的是 timer 的相关寄存器,则通过kvm_riscv_vcpu_set_reg_timerkvm_riscv_vcpu_get_reg_timer来完成;
  • 用户态hypervisor(qemu) 可以获得该vcpu的运行时间(time) 和 compare (t->next_cycles)

guest 访问

主要文件: arch/riscv/kvm/vcpu_timer.c

涉及的主要流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
-+ kvm_arch_init_vm(struct kvm *kvm, unsigned long type)
\ -+ kvm_riscv_guest_timer_init(kvm);
\ -+ gt->time_delta = -get_cycles64();
\ - csr_read(CSR_TIME); "获取 time csr 作为time_delta 值"
-+ kvm_arch_vcpu_create(struct kvm_vcpu *vcpu)
\ -+ kvm_riscv_vcpu_timer_init(vcpu);
\ - kvm_vcpu_timer *t = &vcpu->arch.timer;
| - hrtimer_init(&t->hrt, CLOCK_MONOTONIC, HRTIMER_MODE_REL); "初始化单调timer 定时器, 相对时间"
| - t->hrt.function = kvm_riscv_vcpu_hrtimer_expired; "定时器restart 函数"
-+ kvm_arch_vcpu_load(struct kvm_vcpu *vcpu, int cpu) "vcpu load 时更新 htimedelta"
\ -+ kvm_riscv_vcpu_timer_restore(vcpu);
\ - csr_write(CSR_HTIMEDELTA, (u32)(gt->time_delta)); "更新htimedelta csr"
-+ case SBI_EXT_0_1_SET_TIMER: "guest os ecall 调用sbi, sbi 请求由kvm 接管"
\ - cp = &vcpu->arch.guest_context "guest_context 中保存了guest 运行时的通用寄存器"
| - next_cycle = (u64)cp->a0;
| -+ kvm_riscv_vcpu_timer_next_event(vcpu, next_cycle);
\ -+ kvm_riscv_vcpu_unset_interrupt(vcpu, IRQ_VS_TIMER);
\ - clear_bit(irq, &vcpu->arch.irqs_pending); "irqs_pending 中去除 IRQ_VS_TIMER"
| - delta_ns = kvm_riscv_delta_cycles2ns(ncycles, gt, t);
| - t->next_cycles = ncycles;
| -+ hrtimer_start(&t->hrt, ktime_set(0, delta_ns), HRTIMER_MODE_REL); "启动定时器, 周期为 delta_ns"
\ ----+ kvm_riscv_vcpu_hrtimer_expired(struct hrtimer *h) "定时器到期时, 调用该restart 函数"
\ -|+ if (kvm_riscv_current_cycles(gt) < t->next_cycles) "定时器到期小于ncycles时, "
\ - delta_ns = kvm_riscv_delta_cycles2ns(t->next_cycles, gt, t);
| - hrtimer_forward_now(&t->hrt, ktime_set(0, delta_ns)); "重定时定时器"
| - return HRTIMER_RESTART;
| -|+ else
\ -+ kvm_riscv_vcpu_set_interrupt(vcpu, IRQ_VS_TIMER); "设置 vcpu->arch.irqs_pending IRQ_VS_TIMER"
\ - set_bit(irq, &vcpu->arch.irqs_pending);
| - kvm_vcpu_kick(vcpu);

上述流程有些混乱, 先梳理一下

先回忆一下普通os的tick:

  • 设置time
  • 设置compare
    time 追上compare 后, 触发timer 中断, 在中断处理函数中设置下一轮的compare. 循环往复.

guest timer tick 处理

从前文 kvm_riscv_vcpu_get_reg_timer 中的 KVM_REG_RISCV_TIMER_REG(compare) 来看, 就比较容易理解上述流程了.
os 中调度时, tick 到期后, 需要重新设置compare.
在riscv 中, guest os 设置compare 是需要发 SBI_EXT_0_1_SET_TIMER 的请求
导致陷入到 kvm 中, kvm 接收的请求中第一个参数 next_cycle 即为下一轮的 compare 数.

在guest 因设置 compare 请求导致陷入到 kvm 中, kvm 处理 SBI_EXT_0_1_SET_TIMER:

  • 首先清除了 IRQ_VS_TIMER 中断, 确保本轮tick中不再有有的timer 中断产生
  • 计算下一轮到期的时间 delta_ns (cycle 转换为 host的纳秒单位)
  • 启动定时器, 到期时间为 当前时间+delta_ns
  • 退出 kvm, 重返到 vcpu 运行环境

定时器到期时, 触发中断重新陷入到 kvm 中, 处理函数为 kvm_riscv_vcpu_hrtimer_expired

注意这时并不一定打断了设置compare的 vcpu的运行, 该函数有可能是被其他cpu 异步处理的.

  • 该函数首先判断到期的时间是否是小于 guest 设置的compare, 如果小于的话, 说明时间还没到, 需要重新计算定时时间, 启动定时器
  • 时间到了, 设置对应 vcpu->arch.irqs_pending 加入 IRQ_VS_TIMER, 如果是异步的处理, 调用 kvm_vcpu_kick(vcpu), 让该vcpu 退出运行, 回到kvm.
  • kvm 在下一轮 run vcpu 运行时会检查 vcpu->arch.irqs_pending, 如果有 IRQ_VS_TIMER, 会通过hvip 向该vcpu 注入虚拟timer 中断. 这样vcpu 再运行时, 在guest os中, 就能收到timer 中断, 进而由其 timer 中断处理函数处理. 这个流程可以理解为普通的os的tick到期的情况.

上述过程可见, guest timer tick 处理路径还是比较长的, 效率也比较低

guest time

从上面看更新 htimedelta的地方只有 gt->time_delta , 这个变量的赋值来自于 kvm_arch_init_vm, 为负的vm 实例的创建时间.
所以vcpu的 time 为 mtime+htimedelta = vm的运行时间.

sstc vstimecmp

riscv 支持sstc 后, 增加了vstimecmp的csr

开启/关闭 Sstc 的 stimecmp 功能是通过设置

  • menvcfg.STCE (bit 31) 开启后支持 HS-mode的stimecmp 功能
  • henvcfg.STCE (bit 31) 开启后支持 VS-mode的stimecmp 功能

如果 menvcfg.STCE=0, 则 VS 访问 stimecmp 会导致 illegal insn exception
如果 henvcfg.STCE=0, menvcfg.STCE=1, 则VS访问 stimecmp 会导致virtual insn execption

在Sstc.stimecmp 开启后还要开启 mcounteren 和 hcounteren的TM 位后, guest 才能直接访问 stimecmp 而不会导致异常.
在 mcounteren.tm=1 且 hcounteren.tm=1 时, guest 可以直接访问 stimecmp csr, 而tm位被清0后, guest 访问stimecmp csr 会导致virtual insn exception.

guest 在满足上述 [m/h]envcfg.STCE[m/h]counteren.TM 后可以直接操作 stimecmp csr 来设置timer tick, 就不必再通过sbi 陷入到kvm中设置 compare, 也不需要kvm 维护定时器, 同时也不需要kvm 注入timer 虚拟中断给vcpu, 减少了两次陷入kvm, 大大提升了guest 处理 tick的效率.
tick 到期后, 由硬件来保证vcpu 直接收到timer的虚拟中断, vcpu 不用陷入到kvm 就可以处理一轮轮的tick.

中断虚拟化

本章分两部分:

  • 不支持中断直通, 即只是中断注入的情况
  • 中断直通 aia

plic 中断注入

中断控制器是qemu 模拟出来的
参考前面的介绍

通常 kvm_init函数会调用kvm_irqchip_create函数,后者在vm fd上调用ioctl(KVM_CREATE_IRQCHIP)告诉内核需要在KVM中模拟PLIC中断芯片。
但是会检查 KVM_CAP_IRQCHIP 能力, 这里riscv 并不支持, 所以这条路不会走

1
2
3
-+ kvm_irqchip_create(KVMState *s)
\ - kvm_check_extension(s, KVM_CAP_IRQCHIP) "这个地方检查没过, riscv 并不支持"
| - ret = kvm_arch_irqchip_create(s);

大概看下 qemu中中断注册以及中断控制器模拟的流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
s->irqchip = virt_create_plic(memmap, 0, base_hartid, hart_count);
if (i == 0) {
mmio_irqchip = s->irqchip;
virtio_irqchip = s->irqchip;
pcie_irqchip = s->irqchip;
}

-+ riscv_cpu_init(Object *obj)
\ -+ qdev_init_gpio_in(DEVICE(cpu), riscv_cpu_set_irq, IRQ_LOCAL_MAX + IRQ_LOCAL_GUEST_MAX);
\ - NamedGPIOList *gpio_list = qdev_get_named_gpio_list(DEVICE(cpu), name); "name为NULL"
| -+ gpio_list->in = qemu_extend_irqs(gpio_list->in, gpio_list->num_in, handler, opaque, n);
\ -+ for i in n "n为IRQ_LOCAL_MAX + IRQ_LOCAL_GUEST_MAX 共79"
\ -+ gpio_list->in[i] = qemu_allocate_irq(handler, opaque, i); "新建了79个irq"
\ - irq = IRQ(object_new(TYPE_IRQ));
| - irq->handler = handler;
| - irq->opaque = opaque;
| - irq->n = n;
-+ s->irqchip[i] = virt_create_plic(memmap, i, base_hartid, hart_count);
\ -+ sifive_plic_create(hwaddr addr, char *hart_config, num_harts, hartid_base, num_sources)
\ - DeviceState *dev = qdev_new(TYPE_SIFIVE_PLIC);
| -+ sysbus_realize_and_unref(SYS_BUS_DEVICE(dev), &error_fatal);
\ ---+ sifive_plic_realize(DeviceState *dev) "mmio_irqchip = s->irqchip[i] irq设备的初始化"
\ -+ qdev_init_gpio_in(dev, sifive_plic_irq_request, s->num_sources); "sifive_plic_irq_request 为handler"
\ - NamedGPIOList *gpio_list = qdev_get_named_gpio_list(dev, NULL);
| - gpio_list->in = qemu_extend_irqs(gpio_list->in, gpio_list->num_in, sifive_plic_irq_request, opaque, n);
| -+ qdev_init_gpio_out(dev, s->s_external_irqs, s->num_harts);
\ - NamedGPIOList *gpio_list = qdev_get_named_gpio_list(dev, NULL);
| -+ for i in num_harts
\ - object_property_add_link(OBJECT(dev), propname, TYPE_IRQ, &s->s_external_irqs[i],
object_property_allow_set_link, OBJ_PROP_LINK_STRONG);
| -+ for i in num_harts
\ - CPUState *cpu = qemu_get_cpu(hartid_base + i);
| -|+ if plic->addr_config[j].mode == PLICMode_S
\ -+ qdev_connect_gpio_out(dev, i, qdev_get_gpio_in(DEVICE(cpu), IRQ_S_EXT)); "i 为cpuid"
\ - input_pin = qdev_get_gpio_in(DEVICE(cpu), IRQ_S_EXT)
"找到前面为cpu创建的gpio_list->in[IRQ_S_EXT], IRQ_S_EXT为9, 对应supervisor external interrupt"
| - object_property_set_link(OBJECT(dev), propname, OBJECT(input_pin), &error_abort);
"这个地方建立了链接 object_property_add_link 时 s_external_irqs[i] 指向了 cpu 的gpio_list->in[IRQ_S_EXT]"
"/machine/soc0/harts[0]/unnamed-gpio-in[9]"
-+ serial_mm_init(system_memory, memmap[VIRT_UART0].base, 0,
qdev_get_gpio_in(DEVICE(mmio_irqchip), UART0_IRQ),
399193, serial_hd(0), DEVICE_LITTLE_ENDIAN);
\ -+ qemu_irq irq = qdev_get_gpio_in(DEVICE(mmio_irqchip), UART0_IRQ)
\ -+ NamedGPIOList *gpio_list = qdev_get_named_gpio_list(DEVICE(mmio_irqchip), NULL);
"这个地方会找到 sifive_plic 设备初始化时建的gpio_list"
\ -+ for node in dev->gpios
\ - g_strcmp0(name, ngl->name) == 0
| - return ngl
| - return gpio_list->in[UART0_IRQ];
| -+ SerialMM *smm = SERIAL_MM(qdev_new(TYPE_SERIAL_MM));
\ ---+ serial_mm_realize(DeviceState *dev)
\ -+ sysbus_init_irq(SYS_BUS_DEVICE(smm), &smm->serial.irq);
\ - qdev_init_gpio_out_named(DEVICE(dev), &smm->serial.irq, SYSBUS_DEVICE_GPIO_IRQ, 1);
| -+ sysbus_connect_irq(SYS_BUS_DEVICE(smm), 0, irq);
\ -+ qdev_connect_gpio_out_named(DEVICE(dev), SYSBUS_DEVICE_GPIO_IRQ, n, irq);
\ - object_property_set_link(OBJECT(dev), propname, OBJECT(input_pin), );
"smm->serial.irq link 指向 mmio_irqchip 即 sifive_plic中断控制器的 gpio_list[in]->[UART0_IRQ]"
-+ sifive_plic_irq_request(void *opaque, int irq, int level)
\ - SiFivePLICState *s = opaque;
| - sifive_plic_set_pending(s, irq, level > 0);
| -+ sifive_plic_update(s);
\ - PLICMode mode = plic->addr_config[addrid].mode;
| -+ case PLICMode_S:
\ -+ qemu_set_irq(plic->s_external_irqs[hartid - plic->hartid_base], level);
"s->external_irqs 指向了 cpu的gpio_list->in[IRQ_S_EXT]"
\ -+ irq->handler(irq->opaque, irq->n, level);
\ -+ riscv_cpu_set_irq(opaque, irq, level) "指向了为cpu安装的中断处理函数 riscv_cpu_set_irq"
\ -+ case IRQ_S_EXT:
\ -+ kvm_riscv_set_irq(cpu, irq, level);
\ -+ kvm_vcpu_ioctl(CPU(cpu), KVM_INTERRUPT, &virq);
--------------------------------> 陷入kernel <------------------------------------------------
\ -+ kvm_riscv_vcpu_set_interrupt(vcpu, IRQ_VS_EXT);
\ - set_bit(irq, &vcpu->arch.irqs_pending);
| - set_bit(irq, &vcpu->arch.irqs_pending_mask);
| - kvm_vcpu_kick(vcpu);
"Kick a sleeping VCPU, or a guest VCPU in guest mode, into host kernel mode."
-+ kvm_arch_vcpu_ioctl_run(struct kvm_vcpu *vcpu)
\ -+ kvm_riscv_vcpu_flush_interrupts(vcpu);
\ - val = READ_ONCE(vcpu->arch.irqs_pending[0]) & mask;
| - csr->hvip |= val;
| -+ kvm_riscv_update_hvip(vcpu);
\ - csr_write(CSR_HVIP, csr->hvip);
| - kvm_riscv_vcpu_enter_exit(vcpu); "进入guest"

从上面的过程分析调用链
qemu使用GPIO来实现中断系统

  1. 首先cpu有gpio_in 接口
  2. 中断控制器 sifive_plic 有gpio_in 和 gpio_out, gpio_out 和 cpu 的gpio_in 接口关联
  3. 设备的gpio_out 和 sifive_plic的gpio_in 接口关联
  4. 当有中断发生时, 设备通过gpio_out 通知 sifive_plic, sifive_plic 通过gpio_out 通知 cpu的gpio_in
  5. cpu的gpio_in 的中断处理函数riscv_cpu_set_irq处理最终的中断, 并将中断状态通过kvm ioctl(KVM_INTERRUPT) 命令通知kvm, 由kvm完成最终的中断注入流程, 更新hvip
  6. 进入guest os后(V=1), hvip 注入中断会导致 V=1 时sip 置位, 如果sie相应位置位了, vcpu 会跳到中断入口, guest os 进而处理中断

中断注册(使能)流程

不支持设备直通时, 外设都是qemu 模拟出来的, 外设注册中断也是通过 模拟的中断控制器注册的中断
guest 在request_irq 的最后会通过设置cpu 亲和性使能特定的中断.

使能的逻辑最终会陷入到 sifive_plic_write enable 区域

1
2
//addrid 代表hart的偏移, wordid 代表中断号的偏移
plic->enable[addrid * plic->bitfield_words + wordid] = value;

中断触发流程

我们可以以uart 为入口点, uart调用了qdev_get_gpio_in(DEVICE(mmio_irqchip), UART0_IRQ) 绑定了 UART_IRQ=10的中断号的中断

先看下uart mmio write处理的调用栈, 这里模拟了uart的中断控制器, 写入uart的配置寄存器后, uart需要触发中断给到kvm, kvm再注入到guest.
这个中断的栈是比较重要的, 建议后面再详细看一下, 涉及了serial mmio 配置寄存器模拟的write, qemu调用printf 打印guest的信息, 以及qemu 打印完后, 模拟中断控制器, 将中断回给kvm, kvm最终将中断注入到guest的过程.

qemu中最终的中断处理函数是为 cpu 注册的中断处理函数 riscv_cpu_set_irq

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#0  riscv_cpu_set_irq (opaque=0xaaaaaadc047c80, irq=9, level=1) at ../target/riscv/cpu.c:695
#1 0x00aaaaaadb76d1d8 in qemu_set_irq (irq=0xaaaaaadc05e670, level=1) at ../hw/core/irq.c:45
#2 0x00aaaaaadb645f60 in sifive_plic_update (plic=0xaaaaaadc06aea0) at ../hw/intc/sifive_plic.c:121
#3 0x00aaaaaadb646bc0 in sifive_plic_irq_request (opaque=0xaaaaaadc06aea0, irq=10, level=0) at ../hw/intc/sifive_plic.c:316
#4 0x00aaaaaadb76d1d8 in qemu_set_irq (irq=0xaaaaaadc06c880, level=0) at ../hw/core/irq.c:45
#5 0x00aaaaaadb28c880 in qemu_irq_lower (irq=0xaaaaaadc06c880) at /home/liguang/program/3rdparty/buildroot-2022.08.1/output/build/qemu-7.0.0/include/hw/irq.h:17
#6 0x00aaaaaadb28cfcc in serial_update_irq (s=0xaaaaaadc3c47b0) at ../hw/char/serial.c:144
#7 0x00aaaaaadb28d654 in serial_xmit (s=0xaaaaaadc3c47b0) at ../hw/char/serial.c:251
#8 0x00aaaaaadb28dc04 in serial_ioport_write (opaque=0xaaaaaadc3c47b0, addr=0, val=103,
size=1) at ../hw/char/serial.c:359
#9 0x00aaaaaadb28f268 in serial_mm_write (opaque=0xaaaaaadc3c4490, addr=0, value=103, size=1) at ../hw/char/serial.c:1009
#10 0x00aaaaaadb5dca84 in memory_region_write_accessor (mr=0xaaaaaadc3c4920, addr=0, value=0xffffff8da80008, size=1, shift=0, mask=255, attrs=...) at ../softmmu/memory.c:492
#11 0x00aaaaaadb5dcdc8 in access_with_adjusted_size (addr=0, value=0xffffff8da80008, size=1, access_size_min=1, access_size_max=8, access_fn=0xaaaaaadb5dc970 <memory_region_write_accessor>, mr=0xaaaaaadc3c4920, attrs=...) at ../softmmu/memory.c:554
#12 0x00aaaaaadb5e04c4 in memory_region_dispatch_write (mr=0xaaaaaadc3c4920, addr=0, data=103, op=MO_8, attrs=...) at ../softmmu/memory.c:1514
#13 0x00aaaaaadb5f0df0 in flatview_write_continue (fv=0xaaaaaadc42f380, addr=268435456, attrs=..., ptr=0xffffff8d27f028, len=1, addr1=0, l=1, mr=0xaaaaaadc3c4920) at ../softmmu/physmem.c:2814
#14 0x00aaaaaadb5f0fb4 in flatview_write (fv=0xaaaaaadc42f380, addr=268435456, attrs=..., buf=0xffffff8d27f028, len=1) at ../softmmu/physmem.c:2856
#15 0x00aaaaaadb5f14b4 in address_space_write (as=0xaaaaaadbdd8c48 <address_space_memory>, addr=268435456, attrs=..., buf=0xffffff8d27f028, len=1) at ../softmmu/physmem.c:2952
#16 0x00aaaaaadb5f1558 in address_space_rw (as=0xaaaaaadbdd8c48 <address_space_memory>, addr=268435456, attrs=..., buf=0xffffff8d27f028, len=1, is_write=true) at ../softmmu/physmem.c:2962
#17 0x00aaaaaadb759448 in kvm_cpu_exec (cpu=0xaaaaaadc047c80) at ../accel/kvm/kvm-all.c:2929
#18 0x00aaaaaadb75b1b4 in kvm_vcpu_thread_fn (arg=0xaaaaaadc047c80) at ../accel/kvm/kvm-accel-ops.c:49
#19 0x00aaaaaadb983e94 in qemu_thread_start (args=0xaaaaaadc06ad00) at ../util/qemu-thread-posix.c:556

外设发送中断给cpu时, 在sifive_plic_irq_request 中检查中断的使能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
-+ sifive_plic_irq_request(void *opaque, int irq, int level)
\ -+ sifive_plic_set_pending(s, irq, level > 0);
\ - atomic_set_masked(&plic->pending[irq >> 5], 1 << (irq & 31), -!!level); "设置中断号 对应pending位"
| -+ sifive_plic_update(s);
\ -+ for (addrid = 0; addrid < plic->num_addrs; addrid++) "遍历hart"
\ -+ level = !!sifive_plic_claimed(plic, addrid);
\ -+ for (i = 0; i < plic->bitfield_words; i++) "遍历中断号"
\ - pending_enabled_not_claimed = "检查中断号pending 以及 hart 对应的enable"
(plic->pending[i] & ~plic->claimed[i]) &
plic->enable[addrid * plic->bitfield_words + i];
| -|+ if pending_enabled_not_claimed
\ - return max_irq;
| -|+ else
\ - return 0;
"判断给M-mode 还是给 S-mode, level为0代表关闭中断, level 为 1 代表需要注入中断"
| - uint32_t hartid = plic->addr_config[addrid].hartid;
| -+ case PLICMode_M:
\ - qemu_set_irq(plic->m_external_irqs[hartid - plic->hartid_base], level);
"enable了该中断的hart 需要注入中断, 其他的hart 不需要注入中断, hvip IRQ_VS_EXT 位 置0"
| -+ case PLICMode_S:
\ - qemu_set_irq(plic->s_external_irqs[hartid - plic->hartid_base], level);

guest 中断处理流程

kvm 在 kvm_riscv_vcpu_enter_exit 重新进入guest 后, 先前hvip 注入的IRQ_VS_EXT(10) 号中断转换为 sip IRQ_EXT(9) 号中断,
guest os 直接跳到 stvec 的中断入口的地方.

guest os 在中断处理函数中, 最终需要查中断控制器, 检查是哪一个外设的中断, 该查询操作最终陷入到 sifive_plic_read函数中
落到 pending_base的地址,

1
2
3
4
5
6
static uint64_t sifive_plic_read(void *opaque, hwaddr addr, unsigned size) {
} else if (addr_between(addr, plic->pending_base, plic->num_sources >> 3)) {
uint32_t word = (addr - plic->pending_base) >> 2;
return plic->pending[word];
...
}

查到对应的中断号后, 还需要设置claim, 声明中断已经处理完成.
该操作会落到 context_base 地址, 为写请求, 所以落到 write 函数里.

1
2
3
4
5
6
7
8
9
10
11
static void sifive_plic_write(void *opaque, hwaddr addr, uint64_t value, unsigned size) {
else if (addr_between(addr, plic->context_base,
plic->num_addrs * plic->context_stride)) {
...
} else if (contextid == 4) {
if (value < plic->num_sources) {
sifive_plic_set_claimed(plic, value, false);
sifive_plic_update(plic);
}
...
}

通过 sifive_plic_set_claimed 函数声明中断处理完成, 通过 sifive_plic_update 关闭该hart 上的中断信号, 即清除hvip的IRQ_VS_EXT位.

1
2
bool level = !!sifive_plic_claimed(plic, addrid);
qemu_set_irq(plic->s_external_irqs[hartid - plic->hartid_base], level);

AIA imsic 中断处理

该章节中涉及的代码来自于 https://github.com/avpatel/linux/ riscv_kvm_aia_irqchip_v3 分支

qemu 版本 github 主线 https://github.com/qemu/qemuhttps://github.com/avpatel/qemu
kvmtool 版本 https://github.com/avpatel/kvmtool riscv_aia_v1 分支

使用 qemu linux kernel x86 平台模拟 riscv 主机
使用 kvmtool 编译的 lkvm 执行 guest kernel, guest kernel 和 host kernel 使用的都是 riscv_kvm_aia_irqchip_v3 编译出的kernel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
"host 模拟riscv 主机"
$QEMU -m 2G -nographic \
-M virt,aclint=on,aia=aplic-imsic,aia-guests=3 \
-kernel $KERNEL_BIN \
-drive file=$ROOT_IMAGE,format=raw,id=hd0 \
-device virtio-blk-device,drive=hd0 \
-append "root=/dev/vda rw earlycon=sbi console=ttyS0 crashkernel=64M" \
-smp $SMP \
$DEBUG \
-device virtio-net-device,netdev=net0 \
-netdev user,id=net0,net=192.168.100.1/24,hostfwd=tcp::${HOST_PORT}-:22,hostfwd=tcp::6666-:6666 \
-device virtio-rng-pci \
-device edu \
-bios $OPENSBI_BIN \

"kvmtool 启动 kvm-mode guest"
./lkvm run -k Image -d rootfs.ext2

在介绍imsic 之前, 先看下riscv kvm 的virtual insn exception 的陷入路径
其中guest 读写imsic 相关csr 的模拟会经由这个路径最终转发到kvm进行处理

1
2
3
4
5
6
7
8
9
10
11
12
-+ kvm_riscv_vcpu_exit(struct kvm_vcpu *vcpu, struct kvm_run *run,
struct kvm_cpu_trap *trap)
\ - switch (trap->scause)
| -+ case EXC_VIRTUAL_INST_FAULT: "同时check vcpu->arch.guest_context.hstatus & HSTATUS_SPV"
\ -+ virtual_inst_fault(vcpu, run, trap);
\ - insn = trap->stval;
| - switch ((insn & INSN_OPCODE_MASK) >> INSN_OPCODE_SHIFT)
| -+ case INSN_OPCODE_SYSTEM:
\ -+ system_opcode_insn(vcpu, run, insn);
\ -+ for (i = 0; i < ARRAY_SIZE(system_opcode_funcs); i++) "遍历 system_opcode_funcs"
\ - ifn = &system_opcode_funcs[i]; "(insn & ifn->mask) == ifn->match"
| - ifn->func(vcpu, run, insn) "match ifn->mask opcode 时转入对应的处理函数"

看一下 system_opcode_funcs, 包含了对guest 中因 csrrw csrrs csrrc csrrwi CSRRSI CSRRCI 及 WFI 指令导致异常陷入kvm时的处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
static const struct insn_func system_opcode_funcs[] = {
{
.mask = INSN_MASK_CSRRW,
.match = INSN_MATCH_CSRRW,
.func = csr_insn,
},
{
.mask = INSN_MASK_CSRRS,
.match = INSN_MATCH_CSRRS,
.func = csr_insn,
},
{
.mask = INSN_MASK_CSRRC,
.match = INSN_MATCH_CSRRC,
.func = csr_insn,
},
{
.mask = INSN_MASK_CSRRWI,
.match = INSN_MATCH_CSRRWI,
.func = csr_insn,
},
{
.mask = INSN_MASK_CSRRSI,
.match = INSN_MATCH_CSRRSI,
.func = csr_insn,
},
{
.mask = INSN_MASK_CSRRCI,
.match = INSN_MATCH_CSRRCI,
.func = csr_insn,
},
{
.mask = INSN_MASK_WFI,
.match = INSN_MATCH_WFI,
.func = wfi_insn,
},
};

来看一下 csr_insn

1
2
3
4
5
6
7
8
9
10
-+ csr_insn(struct kvm_vcpu *vcpu, struct kvm_run *run, ulong insn)
\ - csr_num = insn >> SH_RS2; " csrw/csrr 的操作csr"
| - rs1_num = (insn >> SH_RS1) & MASK_RX; "csrw/csrr 中的通用寄存器 index"
| - rs1_val = GET_RS1(insn, &vcpu->arch.guest_context); ""
| - csr_funcs = { .base = CSR_SIREG,      .count = 1, .func = kvm_riscv_vcpu_aia_rmw_ireg }, \
{ .base = CSR_STOPEI,     .count = 1, .func = kvm_riscv_vcpu_aia_rmw_topei },
| -+ for (i = 0; i < ARRAY_SIZE(csr_funcs); i++) "遍历csr_funcs"
\ -|+ if ((tcfn->base <= csr_num) && (csr_num < (tcfn->base + tcfn->count)))
"match 参数传入的csr_num 与 csr_funcs 注册的 csr_num"
\ -+ cfn->func(vcpu, csr_num, &val, new_val, wr_mask) "调用对应的 csr_funcs 注册的函数"

如操作的csr 为 stopei 寄存器, 则会由 kvm_riscv_vcpu_aia_rmw_topei 进行处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-+ kvm_riscv_vcpu_aia_rmw_topei(struct kvm_vcpu *vcpu,
unsigned int csr_num,
unsigned long *val,
unsigned long new_val,
unsigned long wr_mask)
\ -+ kvm_riscv_vcpu_aia_imsic_rmw(vcpu, KVM_RISCV_AIA_IMSIC_TOPEI,
val, new_val, wr_mask) "wr_mask 代表为写请求"
\ -+ imsic_swfile_topei_rmw(vcpu, val, new_val, wr_mask);
\ - imsic *imsic = vcpu->arch.aia.imsic_state; "imsic_state 是保存在vcpu 中的"
\ -+ topei = __imsic_swfile_topei(imsic);
\ -+ for (i = 1; i < max_msi; i++) "max_msi 来自于 swfile->eithreshold 和 imsic->nr_msis"
\ -|+ if (test_bit(i, swfile->eie) && test_bit(i, swfile->eip)) "i 越小, 代表优先级越高, i 从0开始遍历,
eip&eie 的i位都置1了, 说明i位中断pending, 代表最高优先级的中断"
\ - return (i << TOPEI_ID_SHIFT) | i;
| - *val = topei; "读最大优先级的pending 中断"
| - "省略csrw stopei 的处理..."

上述逻辑符合 riscv-aia spec 中的描述, 在csrw/csrr stopei 时需要陷入到kvm中进行处理
这里需要关注下 属于vcpu的 imsic_state 结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
struct imsic_mrif_eix {
unsigned long eip[BITS_PER_TYPE(u64) / BITS_PER_LONG];
unsigned long eie[BITS_PER_TYPE(u64) / BITS_PER_LONG];
};

struct imsic_mrif {
struct imsic_mrif_eix eix[IMSIC_MAX_EIX];
unsigned long eithreshold;
unsigned long eidelivery;
};

struct imsic {
struct kvm_io_device iodev;

u32 nr_msis;
u32 nr_eix;
u32 nr_hw_eix;
/* IMSIC VS-file */
rwlock_t vsfile_lock;
int vsfile_cpu;
int vsfile_hgei;
void __iomem *vsfile_va;
phys_addr_t vsfile_pa;

/* IMSIC SW-file */
struct imsic_mrif *swfile;
phys_addr_t swfile_pa;
};

为什么需要软件维护 swfile 这样的数据

为什么模拟这么一套 imsic的机制进来, 可能是为了兼容 guest os的aia 中断机制.
且imsic 做到了kvm 中进行模拟, 应该比做到qemu 中模拟中断控制器的效率更高.
对应 kvm_irqchip_in_kernel 的情况, qemu 中调用 kvm_riscv_aia_create -> kvm_device_access(aia_fd, KVM_DEV_RISCV_AIA_GRP_CTRL, KVM_DEV_RISCV_AIA_CTRL_INIT, NULL, true, NULL); 对aia 进行初始化, 初始化后
kvm->arch.aia.initialized = true;

在vcpu 退出到kvm后, 下一轮进入guest 前, 会对imsic 的状态进行更新

kvm_riscv_vcpu_aia_update

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
-+ kvm_arch_vcpu_ioctl_run(struct kvm_vcpu *vcpu)
\ -+ while (ret > 0) {
\ - kvm_riscv_vcpu_aia_update(vcpu);
| - kvm_riscv_vcpu_enter_exit(vcpu);
| - kvm_riscv_vcpu_exit(vcpu, run, &trap);

-+ kvm_riscv_vcpu_aia_update(struct kvm_vcpu *vcpu)
\ -+ kvm_riscv_vcpu_aia_imsic_update(vcpu);
\ - kvm_vcpu_aia *vaia = &vcpu->arch.aia_context;
| - imsic = vaia->imsic_state;
| - old_hgei = imsic->hgei;
| - old_hgei_cpu = imsic->hgei_cpu;
| -|+ if (old_hgei_cpu != vcpu->cpu) "初次分配 IMSIC VS-file"
\ - hgctrl = per_cpu_ptr(&aia_hgei, cpu)
| - ret = __ffs(hgctrl->free_bitmap); "返回最低位之后第一个1的位置"
\ -+ kvm_riscv_aia_alloc_hgei(vcpu->cpu, vcpu,
&new_vsfile_va, &new_vsfile_pa); "分配新的IMSIC VS-file"
\ - lc = imsic_get_local_config(cpu); "硬件 imsic 支持, lc 会传回imsic 驱动中的 imsic_local_config"
| - *hgei_va = lc->msi_va + (ret * IMSIC_MMIO_PAGE_SZ);
| - *hgei_pa = lc->msi_pa + (ret * IMSIC_MMIO_PAGE_SZ); "硬件imsic 的guest interrupt file的mmio地址?"
| - new_vsfile_hgei = ret;
| - imsic_vsfile_local_clear(new_hgei, imsic->nr_hw_longs); "清零 IMSIC VS-file"
| - kvm_riscv_gstage_ioremap(kvm, gpa:vcpu->arch.aia_context.imsic_addr, "建立imsic gpa -> hpa 的 G-stage 页表"
hpa:new_vsfile_pa, IMSIC_MMIO_PAGE_SZ, true, true);
"arch.aia.imsic_addr 是由qemu 传过来的, 配置的 memmap[VIRT_IMSIC_S].base"
| - imsic->vsfile_cpu = vcpu->cpu; "imsic 关联物理cpu"
| - imsic->vsfile_va = new_vsfile_va;
| - imsic->vsfile_pa = new_vsfile_pa;
| -+ imsic_swfile_read(vcpu, clear: true, &tmrif); "/* Read and clear register state from IMSIC SW-file */"
"读入 tmrif 临时内存"
\ -+ memcpy(mrif, imsic->swfile, sizeof(*mrif)); "将 imsic->swfile 拷贝给tmrif"
| -+ imsic_vsfile_local_update(new_vsfile_hgei, imsic->nr_hw_eix, &tmrif);
\ - new_hstatus |= ((unsigned long)vsfile_hgei) << HSTATUS_VGEIN_SHIFT;
"更新 hstatus, 注意hstatus.vgein, vsfile_hgei为 vcpu 在 cpu->free_bitmap 的占位"
| - csr_write(CSR_HSTATUS, new_hstatus);
| -+ for (i = 0; i < nr_eix; i++) "nr_eix = imsic->nr_hw_eix 为 eip/eie 的数目, 遍历eip/eie"
\ - eix = &mrif->eix[i];
| -+ imsic_eix_set(IMSIC_EIP0 + i * 2, eix->eip[0]);
"从 imsic->swfile 拷贝到的临时内存中取出eip的值, 最终通过 CSR_VSISELECT CSR_VSIREG 更新真实imsic 中断控制器的 eip,
下eie 的设置同eip"
\ - csr_write(CSR_VSISELECT, __c);
| - csr_set(CSR_VSIREG, __v);
| - imsic_eix_set(IMSIC_EIE0 + i * 2, eix->eie[0]);
| - csr_write(CSR_HSTATUS, old_hstatus); "这个地方又把hstatus 设置回old_hstatus"

简单分析下上面函数的几个重点:

  1. hstatus.vgein的设置, 为物理hart 分配vcpu时, 会在其关联的 free_bitmap 中分配占位, 而该占位的index 可以视为 vcpuid, 将vcpuid 最终存到了hstatus.vgein下
    更新物理imsic 中断控制器的eip/eie 前设置了新的hstatus.vgein, 更新完后又恢复到old hstatus.vgein, 正好证明imsic 控制器的guest interrupt file 是由hstatus.vgein 片选的.
  2. 注意软件维护的imsic->swfile 内存, imsic 数据结构是关联到vcpu的, 而swfile 代表的是软件维护的备份的 guest interrupt file 内存, 设置硬件imsic 的eip/eie 的数据来源为swfile.
  3. 该函数中通过 kvm_riscv_gstage_ioremap 设置了一次 gpa->hpa 的 G-stage 页表, gpa 为 vcpu->arch.aia.imsic_addr, hpa 为从硬件关联的 imsic 中断控制器(drivers/irqchip/riscv-imsic.c) 中 imsic_get_local_config 取到的 msi_pa + vcpu(guest_index) 的 guest interrupt file 的 mmio 地址, 建立了 G-stage 页表后, guest os 访问 qemu 模拟的 imsic 控制器的 mmio 地址就能直接访问到真实的 imsic 的 guest interrupt file 的 mmio 地址上了? 这一块还是有写疑问, 需要后面 check #TODO 这个地方进一步调研后发现建立的 io G-stage 页表并没有带有 PTE_U 的 flag, 而根据 SPEC 的行为, 没有 PTE_U 时, load/store 操作 gva 时会导致 G-stage 翻译失败, 陷入 LOAD/STORE guest page fault. 也就是说 guest os 直接访问 imsic 的地址时, 并没有因为映射了 G-stage 页表就会通过 mmu 的 2-stage 地址转换, 反而还是会陷入 kvm. 那么设置 G-stage 页表的作用是什么呢? #TODO

接下来我们看下更新 kvm 为 vcpu 维护的 imsic->swfile.

guest 有三种方式设置直通的中断

  • guest 通过 csrw/csrr 访问 siselect sireg 设置 eip/eie.
  • guest 通过直接操作 imsic 控制器的 mmio 地址设置 eip/eie, 访问 seteipnum 地址触发中断.
  • 直通的 pcie 设备(走 vfio 驱动) 通过发送 msi 消息触发 msi 的中断.

guest 访问 siselect sireg

这种方式同 访问stopei, guest os 在 csrr/csrw sireg 陷入kvm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| -  new_val = rs1_val;
| - isel = csr_read(CSR_VSISELECT) & ISELECT_MASK; "陷入到kvm后, guest的siselect 转换为host的 viselect,
不用额外处理 guest 中 siselect 的读写?"
-+ kvm_riscv_vcpu_aia_imsic_rmw(struct kvm_vcpu *vcpu, unsigned long isel,
unsigned long *val, unsigned long new_val,
unsigned long wr_mask)
\ - imsic = vcpu->arch.aia_context.imsic_state;
| -+ imsic_mrif_rmw(imsic->swfile, imsic->nr_eix, isel, val, new_val, wr_mask);
\ - switch (isel)
| -+ case IMSIC_EIP0 ... IMSIC_EIP63:
\ - pend = true;
| - num = isel - IMSIC_EIP0;
| - eix = &mrif->eix[num / 2];
| - ei = (pend) ? &eix->eip[0] : &eix->eie[0];
| -+ imsic_mrif_atomic_rmw(imsic->swfile, ei, new_val, wr_mask); "更新imsic->swfile 对应的 eipx 状态"
| - "... 省略eie的处理, 过程同eip"

最终将 eip/eie 的状态保存了kvm维护的vcpu->imsic->swfile中, vcpu 下一轮进入guest 中通过前面介绍的kvm_riscv_vcpu_aia_update 更新到硬件imsic相应的guest interrupt file中.

访问 imsic mmio 由kvm 注入中断

guest 通过访问imsic mmio 地址, 通过 SETIPNUM 地址向某一个vcpu 设置中断.
doorbell 机制, vcpu 没在运行, 此时通过访问 imsic 的 mmio 地址通过 SETIPNUM 向某一个 vcpu 触发中断.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
----->qemu<-------
-+ kvm_riscv_aia_create( "kvm_irqchip_in_kernel 时"
aplic_s, msimode, socket,
VIRT_IRQCHIP_NUM_SOURCES,
hart_count,
memmap[VIRT_APLIC_S].base + socket * memmap[VIRT_APLIC_S].size,
memmap[VIRT_IMSIC_S].base + socket * VIRT_IMSIC_GROUP_MAX_SIZE);
\ - aia_fd = kvm_create_device(kvm_state, KVM_DEV_TYPE_RISCV_AIA, false);
| -+ for (int i = 0; i < hart_count; i++)
\ -+ kvm_device_access(aia_fd, KVM_DEV_RISCV_AIA_GRP_ADDR, KVM_DEV_RISCV_AIA_ADDR_IMSIC(i),
&imsic_addr, true, NULL);
| -+ kvm_device_access(aia_fd, KVM_DEV_RISCV_AIA_GRP_CTRL, KVM_DEV_RISCV_AIA_CTRL_INIT, NULL, true, NULL);
------> kernel <-----------
\ -+ case KVM_DEV_RISCV_AIA_GRP_CTRL:
\ -+ case KVM_DEV_RISCV_AIA_CTRL_INIT:
\ -+ r = aia_init(dev->kvm);
\ -+ kvm_for_each_vcpu(idx, vcpu, kvm) "遍历kvm的所有vcpu"
\ -+ kvm_riscv_vcpu_aia_imsic_init(vcpu);
\ - imsic = kzalloc(sizeof(*imsic), GFP_KERNEL); "为每个vcpu 分配imsic"
| - swfile_page = alloc_pages(GFP_KERNEL | __GFP_ZERO, get_order(sizeof(*imsic->swfile)));
"分配 swfile 内存"
| - imsic->swfile = page_to_virt(swfile_page);
| -+ kvm_iodevice_init(&imsic->iodev, &imsic_iodoev_ops);
"创建 kvm_io_dev, 读写函数为 imsic_mmio_read imsic_mmio_write"
\ - imsic_iodoev_ops = {.read = imsic_mmio_read, .write = imsic_mmio_write}
| - kvm_io_bus_register_dev(kvm, KVM_MMIO_BUS, vcpu->arch.aia_context.imsic_addr,
KVM_DEV_RISCV_IMSIC_SIZE, &imsic->iodev); "将该设备加到 KVM_MMIO_BUS 总线上
地址范围为 [imsic_addr , imsic_addr+ KVM_DEV_RISCV_IMSIC_SIZE]"

"guest 访问 gpa 为 [imsic_addr , imsic_addr+ KVM_DEV_RISCV_IMSIC_SIZE] 的地址范围时, 应该是SETIPNUM 地址
会先由kvm 查找 KVM_MMIO_BUS 上的地址范围, 落在其内, 最终转发给其对应的读写函数, 具体过程参考arm的vgic的处理流程"
1. ->
-+ imsic_mmio_write(struct kvm_vcpu *vcpu, struct kvm_io_device *dev, gpa_t addr, int len, const void *val)
\ - msi.address_hi = addr >> 32;
| - msi.address_lo = (u32)addr;
| - msi.data = *((const u32 *)val);
| -+ kvm_riscv_aia_inject_msi(vcpu->kvm, &msi);
\ - target = (((gpa_t)msi->address_hi) << 32) | msi->address_lo;
| - tppn = target >> IMSIC_MMIO_PAGE_SHIFT;
| - iid = msi->data; "中断号"
| - g = tppn & (BIT(aia->nr_guest_bits) - 1); "通过 imsic 的mmio地址判断 给到哪个 guest interrupt file, 即算出 guest index"
| -+ kvm_for_each_vcpu(idx, vcpu, kvm) "遍历kvm的所有vcpu"
\ - ippn = vcpu->arch.aia_context.imsic_addr >> IMSIC_MMIO_PAGE_SHIFT;
"vcpu的imsic_addr 是特定的, 这个地址是前面 qemu 通过 KVM_DEV_RISCV_AIA_GRP_ADDR 发到kvm的"
| -|+ if (ippn == tppn)
\ - toff = target & (IMSIC_MMIO_PAGE_SZ - 1); "toff 代表 IMSIC_MMIO_SETIPNUM_BE 还是 IMSIC_MMIO_SETIPNUM_LE"
\ -+ kvm_riscv_vcpu_aia_imsic_inject(vcpu, guest_index:g, offset:toff, iid);
\ - iid = (offset == IMSIC_MMIO_SETIPNUM_BE) ? __swab32(iid) : iid;
| - eix = &imsic->swfile->eix[iid / BITS_PER_TYPE(u64)];
| - set_bit(iid & (BITS_PER_TYPE(u64) - 1), eix->eip); "由中断号 iid 定位到对应的eip, 设置swfile的eip"
| -|+ if (imsic->vsfile_cpu >= 0)
\ - writel(iid, imsic->vsfile_va + IMSIC_MMIO_SETIPNUM_LE); "直接写host 真实的中断控制器的 vs interrupt file的内存, 触发vs中断."
| - kvm_vcpu_kick(vcpu);
| -|+ else
\ -+ imsic_swfile_extirq_update(vcpu);
\ - imsic_mrif_topei(imsic->swfile, imsic->nr_eix, imsic->nr_msis))
"更新 vcpu的 imsic->swfile 的 stopei 为对应的中断号和优先级"
| -+ kvm_riscv_vcpu_set_interrupt(vcpu, IRQ_VS_EXT); "直接由hvip 给vcpu 注入中断"
\ - set_bit(irq, vcpu->arch.irqs_pending);
| - kvm_vcpu_kick(vcpu); "将vcpu 踢出来, 退出到kvm, 下一轮进guest 时会通过 kvm_riscv_vcpu_aia_update 函数
将swfile 更新到硬件中断控制器的guest interrupt file 中"

第一条路径更适合于其他vcpu 向某一个vcpu 注入直通的中断

  • 如果可以直接拿到真实 imsic virtual interrupt file 的地址, 该地址是从 host imsic 驱动中拿到的该 vcpu 对应的 cpu 上的 virtual interrupt file 地址, 则可以直接写 SETIPNUM 触发相应的 guest 直通中断.
  • 如拿不到真实 imsic virtual interrupt file 地址, 则更新中断到了 vcpu->imsic->swfile 内存中, 而 kvm_vcpu_kick 将 vcpu 踢出来, 退出到 kvm, 下一轮进 guest 时会通过 kvm_riscv_vcpu_aia_update 函数将 vcpu->imsic->swfile 状态更新到硬件中断控制器的 guest interrupt file 中

值得注意的是, 经过调试发现, guest os 中发送 ipi 中断 (imsic_ipi_send 函数), 也会经过上述路径最终写入到了 virtual interrupt file 的 SETIPNUM, 触发了对应 vcpu 的 guest 中断, 但是最终还是走了 kvm_vcpu_kick 将对应 vcpu 从 sleep 或 guest 运行状态中拖出到 kvm 下? 如果 vcpu 处在 guest 运行时, 不是可以直接触发直通中断吗, 为什么还需要 kick 呢? 这一点不太理解 #TODO

1
2
3
4
5
6
7
8
9
10
11
12
13
2. ->
-+ case KVM_SIGNAL_MSI:
\ -+ kvm_send_userspace_msi(kvm, &msi);
\ - route.msi.address_lo = msi->address_lo;
| - route.msi.address_hi = msi->address_hi;
| - route.msi.data = msi->data;
| - route.msi.devid = msi->devid;
| -+ kvm_set_msi(&route, kvm, KVM_USERSPACE_IRQ_SOURCE_ID, 1, false)
\ - msi.address_lo = e->msi.address_lo;
| - msi.address_hi = e->msi.address_hi;
| - msi.data = e->msi.data;
| - msi.devid = e->msi.devid;
| -+ kvm_riscv_aia_inject_msi(kvm, &msi);

第二条路径中, user_space 通过ioctl(KVM_SIGNAL_MSI) 发送msi 消息过来注册中断, 这个通常是pcie设备的模拟, pcie 设备注册中断时会通过该路径注册中断.

imsic doorbell 中断

在 aia_hgei_init 函数中, 注册了IRQ_S_GEXT (12)的中断处理函数 hgei_interrupt, 在IRQ_S_GEXT 的中断到来时进入 hgei_interrupt 进行处理, 读对应的直通guest中断映射的vcpuid, 唤醒对应的vcpu进入 KVM_RUN的循环中, 进而处理对应的直通中断.
处理imsic->swfile的pending, 注入中断到虚拟机.

1
2
3
4
5
6
7
8
9
10
-+ kvm_riscv_aia_init
\ -+ aia_hgei_init
\ -+ domain = irq_find_matching_fwnode(riscv_get_intc_hwnode(), DOMAIN_BUS_ANY);
\ - hgei_parent_irq = irq_create_mapping(domain, IRQ_S_GEXT); "hwirq 映射的 virq"
| - request_percpu_irq(hgei_parent_irq, hgei_interrupt, "riscv-kvm", &aia_hgei); "doorbell的中断处理函数为 hgei_interrupt"
-+ hgei_interrupt
\ - hgei_mask = csr_read(CSR_HGEIP) & csr_read(CSR_HGEIE);
| - csr_clear(CSR_HGEIE, hgei_mask); "清理hgeie"
| -+ for_each_set_bit(i, &hgei_mask, BITS_PER_LONG)
\ - kvm_vcpu_kick(hgctrl->owners[i]) "hgei_mask 代表vcpuid, 踢vcpu 到 KVM_RUN 循环中, 紧接着处理对应的异常"

qemu MSI/MSIX 信息

当guest os kernel 写入vfio-device的配置空间配置msi/msix时 (向msi/msi-x的capability structure中写入msi/msi-x enable bit), 会陷入到kvm mmio_exit, 进而陷入到qemu中提前注册的处理函数 vfio_pci_write_config
最终下发ioctl KVM_SET_GSI_ROUTING 发给kvm, kvm 经由imsic 注册对应的 kvm_kernel_irq_routing_entry, msi 类型, 其entry的 set 设置为 kvm_set_msi
这一层为 guest的pcie 设备启用msi的中断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
memory_region_init_io(&e->mmio, OBJECT(e), &pcie_mmcfg_ops, e, "pcie-mmcfg-mmio", PCIE_MMCFG_SIZE_MAX);
static const MemoryRegionOps pcie_mmcfg_ops = {
.read = pcie_mmcfg_data_read,
.write = pcie_mmcfg_data_write,
.endianness = DEVICE_LITTLE_ENDIAN,
};
-+ pci_host_config_write_common(pci_dev, addr, limit, val, len);
\ -+ pci_host_config_write_common(pci_dev, addr, limit, val, len);
\ -+ pci_dev->config_write(pci_dev, addr, val, MIN(len, limit - addr));
\ -+ vfio_pci_write_config(pci_dev, addr, val, MIN(len, limit - addr));
\ -|+ if (pdev->cap_present & QEMU_PCI_CAP_MSI
\ -+ vfio_msi_enable(vdev);
\ - vdev->nr_vectors = msi_nr_vectors_allocated(&vdev->pdev);
| -+ for (i = 0; i < vdev->nr_vectors; i++)
\ - VFIOMSIVector *vector = &vdev->msi_vectors[i];
| -+ vfio_add_kvm_msi_virq(vdev, vector, i, false);
\ -+ vector->virq = kvm_irqchip_add_msi_route(&vfio_route_change, vector_n, &vdev->pdev)
\ - virq = kvm_irqchip_get_virq(s);
| - kroute.gsi = virq;
| - kroute.type = KVM_IRQ_ROUTING_MSI;
| - kroute.u.msi.address_lo = (uint32_t)msg.address;
| - kroute.u.msi.data = le32_to_cpu(msg.data);
| - kroute.u.msi.devid = pci_requester_id(dev);
| -+ kvm_add_routing_entry(s, &kroute); "将kroute 写到 KVMState 中"
| - c->changes++;
| -+ vfio_commit_kvm_msi_virq_batch(vdev);
\ -+ kvm_irqchip_commit_route_changes(&c);
\ -|+ if c->changes
\ -+ kvm_irqchip_commit_routes(c->s);
\ - kvm_vm_ioctl(s, KVM_SET_GSI_ROUTING, s->irq_routes);
--------->kernel<-----------------
-+ case KVM_SET_GSI_ROUTING:
\ -|+ if kvm_arch_can_set_irq_routing
\ -+ kvm_set_irq_routing(kvm, entries, routing.nr, routing.flags);
\ -+ for (i = 0; i < nr; ++i)
\ -+ setup_routing_entry(kvm, new, e, ue);
\ -+ kvm_set_routing_entry(kvm, e, ue);
\ - switch (ue->type)
| -+ case KVM_IRQ_ROUTING_MSI:
\ - e->set = kvm_set_msi;
"entry的set函数设置为 kvm_set_msi, 进而接入 aia的中断注册函数"
| - e->msi.address_lo = ue->u.msi.address_lo; "标记地址"
| - e->msi.address_hi = ue->u.msi.address_hi;
| - e->msi.data = ue->u.msi.data; "标记eventid"
| - e->msi.devid = ue->u.msi.devid; "标记 deviceid"
| -+ for (i = 0; i < vdev->nr_vectors; i++)
\ -+ vfio_connect_kvm_msi_virq(&vdev->msi_vectors[i]);
\ - event_notifier_init(&vector->kvm_interrupt, 0) "注册kvm_interrupt的eventfd, 不激活"
| -+ kvm_irqchip_add_irqfd_notifier_gsi(kvm_state, n:&vector->kvm_interrupt, NULL, vector->virq)
\ -+ kvm_irqchip_assign_irqfd(s, event:n, rn, virq, true);
\ - fd = event_notifier_get_fd(event);
| - irqfd = {.fd = fd, .gsi = virq, } "从eventfd 构建 irqfd"
| -+ kvm_vm_ioctl(s, KVM_IRQFD, &irqfd)
------------------->kernel<----------------------------
\ -+ case KVM_IRQFD
\ -+ kvm_irqfd(struct kvm *kvm, struct kvm_irqfd *args)
\ -+ kvm_irqfd_assign(kvm, args);
\ - irqfd->gsi = args->gsi; "gsi 就是 virq"
| - INIT_WORK(&irqfd->inject, irqfd_inject);
"初始化工作队列, 该eventfd对应的处理函数为 irqfd_inject"
| - eventfd = eventfd_ctx_fileget(f.file); "fd 分配eventfd"
| - irqfd->eventfd = eventfd;
| - events = vfs_poll(f.file, &irqfd->pt); "在fd上poll 等待事件"
| -|+ if (events & EPOLLIN)
\ -+ schedule_work(&irqfd->inject); "只考虑有数据,即POLLIN的情形, 调用inject 回调"
\ -+ irqfd_inject(struct work_struct *work)
\ - irqfd = container_of(work, struct kvm_kernel_irqfd, inject);
| - struct kvm *kvm = irqfd->kvm; "找到对应的虚拟机实例"
| - "irqfd_inject函数用来注入使用中断控制器芯片的中断类型,
if判断中断是边沿触发还是水平触发,如果是边沿触发则会调用两次kvm_set_irq"
| -+ kvm_set_irq(kvm, KVM_IRQFD_RESAMPLE_IRQ_SOURCE_ID, irqfd->gsi, 1, false);
\ - i = kvm_irq_map_gsi(kvm, irq_set, irq = irqfd->gsi);
"获取该gsi索引上注册的中断路由项(kvm_kernel_irq_routing_entry),
这个中断路由项是由arch的 kvm_set_routing_entry 注册的"
| -+ while i--
\ - irq_set[i].set(&irq_set[i], kvm, irq_source_id, level, line_status);
"会挨个调用每个中断路由项上的set方法触发中断"
| -+ vfio_enable_vectors(vdev, msix:false);
"构造irq_set, 准备发送给kernel的vfio驱动"
\ - irq_set->flags = VFIO_IRQ_SET_DATA_EVENTFD | VFIO_IRQ_SET_ACTION_TRIGGER;
| - irq_set->index = VFIO_PCI_MSI_IRQ_INDEX
| - fds = (int32_t *)&irq_set->data;
| -+ for (i = 0; i < vdev->nr_vectors; i++)
\ -+ fd = event_notifier_get_fd(&vdev->msi_vectors[i].kvm_interrupt)
| - fds[i] = fd;
| -+ ioctl(vdev->vbasedev.fd, VFIO_DEVICE_SET_IRQS, irq_set); "发起 VFIO_DEVICE_SET_IRQS"
----------->kernel vfio驱动<---------------
\ -+ case VFIO_DEVICE_SET_IRQS
\ -+ vfio_pci_ioctl_set_irqs(vdev, uarg);
"irq_set 的内容填到hdr中"
\ -+ vfio_pci_set_irqs_ioctl(vdev, hdr.flags, hdr.index, hdr.start, hdr.count, data);
\ - switch (index)
| -+ case VFIO_PCI_MSI_IRQ_INDEX:
\ - switch (flags & VFIO_IRQ_SET_ACTION_TYPE_MASK)
| -+ case VFIO_IRQ_SET_ACTION_TRIGGER
\ -+ func = vfio_pci_set_msi_trigger;
| -+ func: vfio_pci_set_msi_trigger(vdev, index, start, count, flags, data);
\ -|+ if (flags & VFIO_IRQ_SET_DATA_EVENTFD)
"vdev->irq_type = VFIO_PCI_MSI_IRQ_INDEX 时"
\ -+ vfio_msi_set_block(vdev, start, count, fds, msix);
\ -+ for (i = 0, j = start; i < count && !ret; i++, j++)
\ - fd = fds[i]
| -+ vfio_msi_set_vector_signal(vdev, j, fd, msix:false);
\ - irq = pci_irq_vector(pdev, vector);
| - trigger = eventfd_ctx_fdget(fd); "获取eventfd"
| - vdev->ctx[vector].name = kasprintf(GFP_KERNEL_ACCOUNT, "vfio-msi%s[%d](%s)",
msix ? "x" : "", vector, pci_name(pdev)); "格式化输出中断名"杯盘占比扩大一定是青光眼吗
| -+ request_irq(irq, vfio_msihandler, 0, vdev->ctx[vector].name, trigger);
"申请中断, irq 为 virq"
"中断处理函数为 vfio_msihandler, trigger 为eventfd"
\ -----+ vfio_msihandler(trigger) "中断到来时, 来到该函数处理"
\ -+ eventfd_signal(trigger, 1); "触发eventfd"
\ ----+ irqfd_inject() "触发 kvm对端 在该eventfd 上poll的线程, 激活其 工作队列, 最终走到 irqfd_inject 函数"
\ - "省略...."
| -+ irq_set[i].set(&irq_set[i], kvm, irq_source_id, level, line_status); "会挨个调用每个中断路由项上的set方法触发中断"
\ -+ e->set: kvm_set_msi(struct kvm_kernel_irq_routing_entry *e, kvm, irq_source_id, level, line_status)
\ -+ kvm_riscv_aia_inject_msi(kvm, &msi); "最终走到 imsic的 中断注入流程"

在 irgfd_inject 中回调 e->set 即 kvm_set_msi 函数, 最终走入 kvm_riscv_aia_inject_msi 中触发对应的中断.
irqfd_inject 函数通过将中断信号发送到指定的IRQ文件描述符来向虚拟机注入中断。虚拟机将接收到该中断信号,并根据中断的类型和处理程序进行相应的处理。

关注下 irqfd 的触发方, 发起方是vfio 注册的中断处理函数
在 vfio 驱动中, 为pcie的每个 vector 注册了一个中断, 中断处理函数为 vfio_msihandler
在 guest 启用 pcie 设备启用该vector 上的中断时, 会因设置pcie的配置空间最终走到 vfio_msi_enable 函数中.

  • 首先为每个vector 注册了一个 irq_entry, 每个entry的set 函数设置为了 kvm_set_msi 函数
  • 为每个vector 注册了一个irqfd, 并开启了一个 work 工作队列, 等待eventfd poll 事件到来, 等到之后进入 irqfd_inject 函数处理
  • 为每个vector 申请了一个中断, 中断处理函数为 vfio_msihandler, 中断名”vfio-msi[i]”
  • 在pcie 设备因中断到来时, 先触发host kernel的该函数, 写vector绑定的eventfd
  • 写vector绑定的eventfd 会导致 irqfd 对应的work 工作队列被激活, 最终进入 irqfd_inject 函数路由相关的中断entry, 最终走到 imsic的中断注入函数中 kvm_set_msi->kvm_riscv_aia_inject_msi
  • kvm_riscv_aia_inject_msi 会解析msi消息, 判断target 是给到哪个vcpu的, 访问的是不是 setipnum 地址, 如果是setipnum, 则更新swfile->stopei, 更新swfile eip, 开启对应的guest 直通的中断, hvip 注入中断给vcpu, 将vcpu 唤醒